Skip to content

fix(blocks): destroy the Tool of a Block that leaves the collection - #3034

Open
Minhal128 wants to merge 1 commit into
codex-team:nextfrom
Minhal128:fix/destroy-replaced-blocks
Open

Minhal128 wants to merge 1 commit into
codex-team:nextfrom
Minhal128:fix/destroy-replaced-blocks

Conversation

@Minhal128

Copy link
Copy Markdown

Closes #3029

The problem

Block has a destroy() that tears down its listeners, its mutation observer and the Tool instance. But three of the paths where a Block leaves the collection never called it:

path reached by
Blocks.replace() every blocks.update()
Blocks.insert(..., replace = true) conversion, Toolbox / shortcut on an empty Block, paste into an empty Block
Blocks.removeAll() blocks.clear()

In each case the Block is detached from the DOM and dropped from the array while its Tool keeps its input listeners and MutationObserver alive. A document that is converted or updated a few hundred times accumulates one live Tool per operation.

Not what the issue says

The issue reports that destroy() is "never called anywhere". That part is not accurate, and it matters: BlockManager.removeBlock() (blockManager.ts) already calls block.destroy() right after this._blocks.remove(index). Adding a destroy() inside Blocks.remove() on top of that would have called every Tool's destroy() twice.

The change

Rather than patching each caller, the cleanup now lives where a Block actually leaves the collection:

  • Blocks.remove() destroys the Block it splices out — which also fixes BlockManager.removeAllBlocks(), since that loops over _blocks.remove(index)
  • the duplicate block.destroy() in BlockManager.removeBlock() is dropped, so a Tool is still destroyed exactly once
  • Blocks.replace(), Blocks.insert(..., replace = true) and Blocks.removeAll() destroy the Block they drop

destroy() is always called after the REMOVED hook, so a Tool still observes removal before being torn down.

I checked that Blocks.move() and Blocks.swap() reuse the Block instance without going through remove(), so moving a Block does not destroy it.

Verification

Added a Cypress spec asserting that blocks.update() destroys the Tool of the Block it replaces. Ran the suite on a fork in both directions:

branch firefox chrome edge
test only, without the fix ❌ expected onDestroy to have been called exactly once, but it was never called (3 attempts) — —
test + fix ✅ ✅ ✅

So the spec genuinely reproduces the leak and the fix closes it, and nothing else in the suite regressed.

Blocks.replace(), Blocks.insert(..., replace = true) and Blocks.removeAll()
dropped their Block without ever calling Block.destroy(), so the Tool
instance kept its listeners and mutation observer alive. Every
blocks.update(), every conversion and every replace-on-empty-block leaked
one Tool.

Destroy the Block where it actually leaves the collection instead of in
one caller: Blocks.remove() now owns the cleanup, which also covers
BlockManager.removeAllBlocks(), and the duplicate destroy() call in
BlockManager.removeBlock() is dropped so a Tool is still destroyed exactly
once.
Copilot AI lite review requested due to automatic review settings September 24, 2026 14:33

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@neSpecc neSpecc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems ok. Please, cover all block removing cases with tests.
Also, increment a patch version and drop a line in a changelog

This branch has not been deployed

No deployments
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.

Complete Omission of block.destroy() in Blocks Collection

3 participants