Repository navigation
[lexical-website][lexical-y] Documentation Update: Record the 10/31 decisions on undo presses, list numbering, overlapping formats, rewritten words and provider ordering #9685
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -895,8 +895,8 @@ merge the `update` it returns into the live document (see | |
| ## Servers | ||
|
|
||
| A Yjs 14 (`@y/y`) server or relay can host the documents when it meets | ||
| the requirements below (format cleanup off, updates delivered in order, and | ||
| for yrs, its version and client ids). Clients of | ||
| the requirements below (format cleanup off, updates delivered in causal | ||
| order, and for yrs, its version and client ids). Clients of | ||
| `@lexical/yjs` write to other roots (`root`, `root-v2`), never to | ||
| `DEFAULT_ROOT_NAME` (`'lexical-y'`), so they cannot change a `@lexical/y` | ||
| document's content and the server needs no check to keep them out. | ||
|
|
@@ -960,6 +960,20 @@ the document's client id when it is created: | |
| doc.clientID = crypto.getRandomValues(new Uint32Array(1))[0] || 1; | ||
| ``` | ||
|
|
||
| Deliver updates in causal order: each update after every update it depends | ||
| on, from any client (as json-joy also requires). A single relay that every | ||
| client sends to and receives from, and that forwards every update to each | ||
| client in the one order it received them, whichever client sent it, does | ||
| this. A mesh (such as y-webrtc), or | ||
| several relays that forward updates on their own, needs causal buffering: | ||
| hold an update until every update it depends on has been applied, deletions | ||
| included (a state vector alone cannot tell: an update that only deletes | ||
| leaves it unchanged). Out of causal order, a | ||
| client can see a collaborator's format run on to the end of a line until the | ||
| missing update arrives, and an edit it makes in that moment makes the | ||
| spread permanent, on every client | ||
| ([#9645](https://github.com/meta/lexical/issues/9645)). | ||
|
|
||
| Deliver each client's updates in order. With Yjs 14.0.0-rc.29, a client | ||
| that receives an update past a gap in another client's clocks (a relay that | ||
| reorders and batches updates) can show a character that a document loaded | ||
|
|
@@ -1169,6 +1183,15 @@ not merge as a user might expect: | |
| - **List type change ‖ new item.** Changing a list's type while a | ||
| collaborator adds an item to it (Enter in the list) splits the list: the | ||
| new item keeps the old type, in a list of its own between the two halves. | ||
| - **List numbering after refused follow-ups.** After four follow-ups in a | ||
| row that the binding does not write and reads again (such as | ||
| `BlockIdExtension`'s naming updates), the author's editor can show a list | ||
| number from before them (1 where its document reads 2) until that list | ||
| is refreshed: adding an item to it does, an edit elsewhere or retyping an | ||
| item's text does not. The binding stops | ||
| refreshing derived numbers on the fourth such re-read in a row, so that an | ||
| app listener that answers every refresh cannot loop forever. Other | ||
| clients, the stored document and a reload show the right number. | ||
| - **Blocks that arrive without an id.** With `BlockIdExtension`, a block | ||
| stored without an id (written by a client without the extension), or with | ||
| an id an earlier block also stores or shows (a copy of the JSON of a block | ||
|
|
@@ -1235,7 +1258,12 @@ not merge as a user might expect: | |
| format, and stays on its content: | ||
| [#9627](https://github.com/meta/lexical/issues/9627). Where a collaborator overwrote a step of yours | ||
| entirely, the undo manager skips that step as changing nothing, and the | ||
| same undo also reverts your step before it. When you undo a format change | ||
| same undo also reverts your step before it. An undo press can show | ||
| nothing: when its step only restores or removes format items no reader | ||
| shows (a format on text a collaborator then deleted), the press takes that | ||
| step alone, and several such steps in a row take several presses before | ||
| one shows a change, as with Yjs's `UndoManager` in any editor that uses it | ||
| as it is. When you undo a format change | ||
| while a collaborator removes that format and undoes the removal, the text | ||
| keeps the format: the collaborator's undo brings it back. A format a collaborator applies | ||
| starting right where a run of that format ends, or from inside the run | ||
|
|
@@ -1308,8 +1336,13 @@ not merge as a user might expect: | |
| after a Backspace it takes the type of the line before (a paragraph after | ||
| a heading becomes a heading), after undoing a split it keeps its own. | ||
| Bold ‖ bold on adjacent ranges of one line | ||
| can leave part unformatted, and so can bold ‖ bold on overlapping ranges | ||
| ([#9626](https://github.com/meta/lexical/issues/9626)). A link behaves | ||
| can leave part unformatted, and so can the same format (bold, the same | ||
| comment id, a link) applied by two collaborators to overlapping ranges at | ||
| once | ||
| ([#9626](https://github.com/meta/lexical/issues/9626)): a Yjs text gives | ||
| each place one value per key | ||
| ([yjs/yjs#382](https://github.com/yjs/yjs/issues/382)), so one writer's | ||
| end of its range can end the other's run there too. A link behaves | ||
| like a comment there: a range that crosses a link's edge can lose part | ||
| of its bold for either client id order, and a bold reaching into a | ||
| link's first word can lose it there when a collaborator one update | ||
|
|
@@ -1324,7 +1357,13 @@ not merge as a user might expect: | |
| takes that format (also when the range comes from an older read, as an | ||
| agent's does), and text typed at the start or the end of the range can | ||
| take it, by the order Yjs gives the two inserts (at the start the same on | ||
| Yjs 13, and it keeps the format when that range is then deleted). A | ||
| Yjs 13, and it keeps the format when that range is then deleted). In the | ||
| same way, a comment on a word a collaborator rewrites at the same moment | ||
| can land on the new word: with `setTextContent` for one client id order, | ||
| and with a rewrite of the whole line (its text nodes replaced by a new | ||
| one, as an agent writes it) for one or both orders; the words of the range | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: "one or both orders" could say which. A block rewrite moves the comment for both client id orders when the commented range continues past the rewritten word (
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed; the next docs PR names them: both orders when the range continues past the rewritten word, one order when it ends there. |
||
| the rewrite kept keep the comment | ||
| ([#9610](https://github.com/meta/lexical/issues/9610)). A | ||
| node whose `isInline()` changes is written again in its new form, so | ||
| text typed inside it at the same time lands next to it, in an inline | ||
| element of its old form. Where text was deleted, the format items it | ||
|
|
@@ -1366,6 +1405,12 @@ reproduces it: | |
| container and makes no list, in every `@lexical/y` editor (it runs | ||
| `NodeStructureExtension`): | ||
| [#9570](https://github.com/meta/lexical/issues/9570). | ||
| - Undoing plain text typed into a format run gives the run's format (or | ||
| comment, or style) to plain text a collaborator typed right next to it: | ||
| [#9681](https://github.com/meta/lexical/issues/9681). | ||
| - With Yjs garbage collection on, undoing a format removal while a | ||
| collaborator applies the same format again can take it off the next | ||
| character: [#9683](https://github.com/meta/lexical/issues/9683). | ||
|
|
||
| For `MarkStateExtension` comments, see the | ||
| [comments limitations](./comments.md#limitations). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,6 +102,7 @@ issues and docs that carry it. | |
| | D45 | **`editYState` requires a `writer`** (`createYWriter`, carrying an author) or `null`. A writer keeps one client id per document while each call continues from its last update; when that is not safe (an older state, a call inside a call) it takes a new id and warns. A caller never picks a client id. | Every server edit decides who writes it. A client id reused by two writers corrupts silently, and a new id per edit bloats the document. | `DECIDED` | 10-28 | [#9437], [y-server.md][y-server] | | ||
| | D71 | **Server-side edits stay callbacks** (`editYState`'s `$edit`), run on the state the edit was computed from, with guidance added on writing them. | The edit is ordinary Lexical code with every Lexical API available; guidance covers what the callback cannot enforce. | `DECIDED` | 10-25 | [#9437], [y.md: Edits made by code][y-code] | | ||
| | D46 | **Products own server policy; Lexical gives the pieces.** How an agent's edit applies (at once, as a suggestion, with an undo) and what a refused client does are the product's choice; the framework dictates no extra steps. Relay checks are defence in depth for authorized writers, not a permission boundary. | Products differ, and a server cannot judge a peer's intent from a Yjs update. | `DECIDED` | 10-29 | | | ||
| | D92 | **Providers deliver updates in causal order:** each update after every update it depends on, from any client, as json-joy also requires. A single relay that every client sends to and receives from, and that forwards every update to each client in the one order it received them, whichever client sent it, does this; a mesh (y-webrtc) or several relays need causal buffering that tracks each update's dependencies, deletions included (a state vector does not change for an update that only deletes). | Yjs holds back what it cannot place yet: out of causal order, a collaborator's format can run on to the end of a line until the missing update arrives, and an edit made in that moment makes the spread permanent ([#9645]). Ordering is the transport's job; the binding cannot write while applying remote updates. | `DECIDED` | 10-31 | [#9645], [y.md: Servers][y-servers] | | ||
|
|
||
| ## Accepted limits | ||
|
|
||
|
|
@@ -120,6 +121,10 @@ issues and docs that carry it. | |
| | D57 | **Lone surrogates** reach other clients as U+FFFD. | Yjs encodes text as UTF-8; Yjs 13 does the same. Malformed text such as a broken emoji is the author's own input, not an editor problem, so there is no code follow-up. | `ACCEPTED LIMIT` | 10-30 | [limits] | | ||
| | D58 | **Inline elements with their own content** (`MarkNode`, `AutoLinkNode`) move text into them when they wrap it, so concurrent typing there stays outside. | Use NodeState marks (D37). | `ACCEPTED LIMIT` | 10-20 | [limits] | | ||
| | D79 | **The Yjs guard (D14) cannot tell every other Yjs apart:** `@y/y` exposes no version at run time, so the guard checks shape and the peer dependency enforces the exact version; a `@y/y` with the same exports, members and events passes (`14.0.0-rc.25` to `rc.28` do), as does one whose functions behave differently with the same shape, and a provider that bundles its own copy of Yjs and applies updates to the app's document after the binding started is not caught. | `@y/y` exposes no version at run time, and checking updates would cost on every one. The exact peer pin and the docs (install the named version once; `npm ls @y/y`) cover them. | `ACCEPTED LIMIT` | 10-30 | [#9607], [limits] | | ||
| | D81 | **An undo press can show nothing:** when the step it takes only restores or removes format items no reader shows (a format on text a collaborator then deleted), the press takes that step alone; several such steps in a row take several presses before one shows a change. | Yjs's `UndoManager` skips only a step that changes no item, and an editor that uses it as it is behaves the same. Skipping such steps too ([#9630]) needed a digest of what readers show before and after every step: a cost on every press, and a digest that collides would skip a step that does change something. | `ACCEPTED LIMIT` | 10-31 | [#9630], [limits] | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: D90 and D93 are pinned by
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed; the next lexical-y PR brings a pinned repro naming D81 (from #9630's UndoInvisibleSteps, with its main control). |
||
| | D90 | **List numbering can lag after refused follow-ups:** after four follow-ups in a row that the binding does not write and reads again (such as `BlockIdExtension`'s naming updates), the author can show a derived list number from before them (1 where its document reads 2) until that list is refreshed (adding an item to it does; an edit elsewhere, or retyping an item's text, does not); its own fresh read, other clients and a reload show the right number. | The stop on the fourth re-read in a row is the backstop that ends an app's reply loops: [#9634] showed that fresh derived values after every re-read, bounded reply chains and app replies that read the derived value cannot all hold. Display only, the author's own editor, no stored change. | `ACCEPTED LIMIT` | 10-31 | [#9632], [#9634], [#9642], [limits] | | ||
| | D91 | **Overlapping formats of one key from two writers can cancel:** two collaborators applying the same format (bold, the same comment id, a link) to overlapping ranges at once can leave part of the text without it. Since [#9601] this also reaches a link's edge and its first word (D82). | A Yjs text gives each place one value per key ([yjs#382]): a writer's end of its range ends the other writer's run there too. Avoiding it needs a marker per writer, a storage change. Formatting only, no text lost; [#9601] is kept. | `ACCEPTED LIMIT` | 10-31 | [#9626], [#9601], [limits] | | ||
| | D93 | **A comment on a word a collaborator rewrites at the same moment can land on the new word:** with `setTextContent` for one client id order, and with a block rewrite (the line's children replaced by one new text node, as an agent writes it) for one or both orders; words of the range the rewrite kept keep the comment. | The same race as text typed at the start of a range a collaborator formats (y.md, Other races): the new word is inserted where the commented word starts, and Yjs orders the two writes by client id. Before [#9598] a block rewrite dropped the comment from the whole range instead. Formatting only, no text lost. | `ACCEPTED LIMIT` | 10-31 | [#9610], [limits] | | ||
|
|
||
| ## Open | ||
|
|
||
|
|
@@ -130,16 +135,16 @@ issues and docs that carry it. | |
| | D61 | **Two concurrent joins into one line** both write its type; Yjs keeps one by client id. | Inherent to the marker layout; a real trade-off. | `OPEN` | 10-30 | [#9511] | | ||
| | D62 | **Two app versions that store a type differently insert its first node at once.** | Needs a storage-kind record protocol; `schemaVersion` (D08) covers apps that bump it. | `OPEN` | 10-30 | [#9512] | | ||
| | D63 | **A line without a marker joins a paragraph a collaborator appends.** | Every reader rule considered changes how stored documents read. | `OPEN` | 10-30 | [#9524] | | ||
| | D64 | **Undoing a format while a collaborator edits inside its range.** | Yjs format items cannot be conditional on another item; the remaining option is a core or storage change. | `OPEN` | 10-30 | [#9560], [#9521] | | ||
| | D64 | **Undoing a format while a collaborator edits inside its range.** | Yjs format items cannot be conditional on another item; the remaining option is a core or storage change. | `OPEN` | 10-30 | [#9560], [#9521], [#9682] | | ||
| | D65 | **A comment spreading after an offline client types plain text into it.** | Since D86 a delete keeps every format item it leaves, so the [#9609] cells where a collaborator's delete took the run's end pass; the cells where the restore's run has no end of its own (`OwnRunRestoreEnd.test.ts`) still fail. [#9562], which extended the restore's end rule to runs the writer opened itself, is parked: each attempt regressed a case main handles. The case is tracked in [#9609], with the tests moving to main in [#9605]. A restore in a run whose format change wrote more than 64 items gets no end, because the end scan stops there ([#9603]). | `OPEN` | 10-30 | [#9564], [#9609], [#9562], [#9605], [#9603] | | ||
| | D66 | **Server identity:** claim a client id on first use, or server-issued leases. | Claim on first use is the recommendation; not decided. | `OPEN` | 10-29 | | | ||
| | D67 | **The rest of the server-defence proposals:** a relay gate judging version records against a published ceiling, budgets, recovery, an author map, agent write policies. | Researched and proposed; nothing decided beyond D46. | `OPEN` | 10-29 | | | ||
| | D68 | **Splice-based text edit helpers exported from Lexical** for agents that rewrite text, and named APIs for intent that `setTextContent` drops. | Agents should edit text in place; the API shape is undecided. | `OPEN` | 10-27 | [#9483] | | ||
| | D69 | **Comments on block decorators** (an image), as in Google Docs. | Not today, same as `MarkNode`; keep the door open. | `OPEN` | 10-23 | [#9380] | | ||
| | D74 | **Keeping the run ends of text an undo removes from inside a format run.** | [#9561] keeps them, but regressed a concurrent undo ([#9593]) and is parked. Option C: settle the kept ends inside the undo that makes them stale, where the undoing client has seen them; the truly concurrent case would stay a documented limit. Its tests move to main in [#9605]. | `OPEN` | 10-30 | [#9561], [#9593], [#9605] | | ||
| | D74 | **Keeping the run ends of text an undo removes from inside a format run.** | [#9561] keeps them, but regressed a concurrent undo ([#9593]) and is parked. Option C: settle the kept ends inside the undo that makes them stale, where the undoing client has seen them; the truly concurrent case would stay a documented limit. Its tests move to main in [#9605]. | `OPEN` | 10-30 | [#9561], [#9593], [#9605], [#9681] | | ||
| | D75 | **The table padding contract under collaboration.** When every client re-runs the table transforms after remote changes, concurrent pads add cells and the table never settles. | Padding that converges needs a rule: only the client that created a row pads it, or a width that is a function of the document instead of each client's write. Meanwhile the open [#9539] would pad, when an edit leaves the table's rows in place, only the rows that lost cells. Two clients padding at once is the accepted limit D55. | `OPEN` | 10-30 | [#9582], [#9539] | | ||
| | D76 | **A code-token-aware trim for remade text.** A stale replace of a word can take the first letter of the word a rewrite made from it, where a node boundary cut the old word. | Writing a changed word whole fixes it, but also rewrites highlighter tokens glued to a pasted-over token in a code line. A follow-up would apply the rule everywhere except at code highlight token boundaries (D29). | `OPEN` | 10-30 | [#9608] | | ||
| | D77 | **A stale comment on a word a collaborator rewrites** lands on the new word for one client id order only. | Likely the documented race where text typed at the start of a range a collaborator formats takes that format, with a comment instead of a format. Either the docs name comments and whole-word replacements, or a fix makes both orders agree. | `OPEN` | 10-30 | [#9610], [limits] | | ||
| | D77 | **A stale comment on a word a collaborator rewrites** lands on the new word for one client id order only. | Likely the documented race where text typed at the start of a range a collaborator formats takes that format, with a comment instead of a format. Either the docs name comments and whole-word replacements, or a fix makes both orders agree. Replaced by D93 (10-31): documented as a limit. | `DECIDED` | 10-31 | [#9610], [limits] | | ||
|
|
||
| [root-agents]: ../../AGENTS.md | ||
| [limits]: ../lexical-website/docs/collaboration/y.md#limits | ||
|
|
@@ -213,3 +218,11 @@ issues and docs that carry it. | |
| [#9597]: https://github.com/meta/lexical/issues/9597 | ||
| [#9606]: https://github.com/meta/lexical/issues/9606 | ||
| [#9666]: https://github.com/meta/lexical/issues/9666 | ||
| [#9630]: https://github.com/meta/lexical/pull/9630 | ||
| [#9632]: https://github.com/meta/lexical/issues/9632 | ||
| [#9634]: https://github.com/meta/lexical/pull/9634 | ||
| [#9642]: https://github.com/meta/lexical/pull/9642 | ||
| [#9645]: https://github.com/meta/lexical/issues/9645 | ||
| [#9681]: https://github.com/meta/lexical/issues/9681 | ||
| [#9682]: https://github.com/meta/lexical/issues/9682 | ||
| [yjs#382]: https://github.com/yjs/yjs/issues/382 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: this paragraph now follows the causal-order requirement above, which already includes each client's own order (#9645 quotes this sentence as the rule that was not enough). Opening it with something like "Causal order includes each client's own order: with Yjs 14.0.0-rc.29, ..." would keep it from reading as a second, separate rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed; folded into the next docs PR (agent-edit guidance), which touches the same page, so this one can land as approved.