Skip to content

fix: wrap history withMerging, withNewBatch, and withoutMerging in try/finally to ensure state cleanup on error#6063

Open
suyash-vyas wants to merge 3 commits into
ianstormtaylor:mainfrom
suyash-vyas:fix/history-try-finally-cleanup
Open

fix: wrap history withMerging, withNewBatch, and withoutMerging in try/finally to ensure state cleanup on error#6063
suyash-vyas wants to merge 3 commits into
ianstormtaylor:mainfrom
suyash-vyas:fix/history-try-finally-cleanup

Conversation

@suyash-vyas

@suyash-vyas suyash-vyas commented May 24, 2026

Copy link
Copy Markdown

Description

HistoryEditor.withMerging, HistoryEditor.withNewBatch, and HistoryEditor.withoutMerging set internal state flags (MERGING, SPLITTING_ONCE) on WeakMaps before calling the user-provided fn(), then restore the previous state afterward. However, if fn() throws an error, the state restoration code is never reached, leaving the editor permanently stuck in a corrupted internal state (e.g., all subsequent operations are merged into a single undo batch, or splitting is permanently enabled).

This fix wraps the fn() call and state restoration in try/finally blocks, ensuring the editor's internal state is always correctly restored regardless of whether fn() succeeds or throws.

Issue

Issue N/A — Discovered via code inspection. (Follows up on PR #5837 which fixed the same bug for withoutSaving).

Context

The fix follows the exact same pattern already established by withoutSaving in the same file (history-editor.ts), as well as withoutNormalizing in the core slate package (without-normalizing.ts). Both of these methods wrap fn() in try/finally to protect against exceptions. This change simply applies the same defensive pattern to the three remaining methods that were missing it.

The change is purely defensive — when fn() succeeds (the normal case), behaviour is identical to before.

Checks

  • The new code matches the existing patterns and styles.
  • The tests pass with yarn test.
  • The linter passes with yarn lint. (Fix errors with yarn fix.)
  • The relevant examples still work. (Run examples with yarn start.)
  • You've added a changeset if changing functionality. (Add one with yarn changeset add.)

@changeset-bot

changeset-bot Bot commented May 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 37b2a9d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
slate-history Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@suyash-vyas suyash-vyas force-pushed the fix/history-try-finally-cleanup branch from a04d27a to 0d02a9f Compare May 24, 2026 18:24
@suyash-vyas suyash-vyas marked this pull request as ready for review May 24, 2026 18:24
@zbeyens

zbeyens commented May 29, 2026

Copy link
Copy Markdown
Contributor

Findings

  • P2 packages/slate-history/src/history-editor.ts:105: withNewBatch now always deletes SPLITTING_ONCE in finally, but it never records the previous splitting state. In a nested withNewBatch, if the inner callback throws before any operation and the outer callback catches it, this clears the outer pending split, so the next outer operation can merge when it should start a new batch. Capture/restore the prior splitting state, or only clear the flag when this call actually consumed it.

@suyash-vyas

Copy link
Copy Markdown
Author

Findings

  • P2 packages/slate-history/src/history-editor.ts:105: withNewBatch now always deletes SPLITTING_ONCE in finally, but it never records the previous splitting state. In a nested withNewBatch, if the inner callback throws before any operation and the outer callback catches it, this clears the outer pending split, so the next outer operation can merge when it should start a new batch. Capture/restore the prior splitting state, or only clear the flag when this call actually consumed it.

Tried addressing your findings can you please check again.

@zbeyens

zbeyens commented May 31, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37b2a9de91

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fn()
} finally {
MERGING.set(editor, prev)
SPLITTING_ONCE.set(editor, prevSplitting)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not re-arm the outer split flag after nested batches

When withNewBatch is nested inside another withNewBatch before the outer callback has applied an operation, prevSplitting is true; if the inner callback applies an operation, with-history consumes that flag by setting SPLITTING_ONCE back to undefined, but this finally block restores it to true. The next operation in the outer callback is then forced into another new undo batch even though it is a subsequent operation and should merge as usual, regressing nested batch behavior introduced by this cleanup change.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants