Skip to content

Bug: [lexical-y] A write that fails after inserting part of a change is recovered by inserting that content again #9775

Open

Description

@zurfyx

Description

When a local write throws part way, @lexical/y rewrites every container
in a fresh transaction (RECOVERED_WRITE_WARNING). The rewrite diffs the
editor against the binding's record of the document, which is updated only
when a container's write completes. Content the failed attempt had already
inserted is in the document but not in that record, so the recovery
inserts it again: the document stores the text twice while the editor that
made the change shows it once. A fresh client loading the stored state
shows both copies.

Reproduction

On main (gc on or off):

  1. Bind an editor to a document whose root holds one paragraph abc.
  2. Register a NodeState (createState) whose unparse throws once while
    the binding's write transaction is open (doc._transaction.origin === binding).
  3. In one editor.update, append a paragraph mine with that state set.

The paragraph's text and end marker are inserted before its marker fields
(which run the serializer) are computed; the serializer throws, the
recovery runs.

Expected

The document stores abcmine once, as the editor shows.

Actual

The document stores abcminemine; Y.encodeStateAsUpdate loaded into a
fresh Y.Doc reads abcminemine. The editor shows abcmine.

Controls

  • The same edit without a throwing serializer stores abcmine.
  • A serializer that throws before anything is inserted (no partial write)
    stores abcmine.
  • Any failure inside the writer after it inserted content and before the
    container's record is updated reproduces it (for example a block node's
    serializer, run after the line before it was inserted, or a nested
    container's write).

Notes

A fix needs the recovery to see what the failed attempt inserted: rebuild
the affected containers' record from the document before the rewrite (the
reader's unit parser can do it), treating only units made of items the
failed writer inserted as its own, so an app's write inside the binding's
transaction is neither deleted nor duplicated.

Activity

  1. zurfyx commented on Nov 4, 2026

    @zurfyx
    MemberAuthor

    More shapes that reproduce it on main, all with a NodeState unparse that throws once inside the binding's write transaction: an inline decorator's state (its text before it is already inserted), the state of a paragraph or block inside a newly inserted container (the container is already inserted), a table cell's state in a new table, and a named slot's value (the slot is already inserted). In each case the recovery stores the partly written content a second time; the editor shows it once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions