Skip to content

Complete Omission of block.destroy() in Blocks CollectionΒ #3029

Description

@codeCraft-Ritik

1. Complete Omission of block.destroy() in Blocks Collection

πŸ“ Affected Locations

  • src/components/blocks.ts (Lines 200–205, 230–240, 285–295, 300–306)
  • src/components/block/index.ts (Lines 688–698)

πŸ” Deep Technical Diagnosis

The Block class defines an essential cleanup method in src/components/block/index.ts:

// src/components/block/index.ts: Lines 688-698
public destroy(): void {
  this.holder.removeEventListener('change', this.handleSelectChange);
  this.unwatchBlockMutations();
  this.removeInputEvents();

  super.destroy();

  if (_.isFunction(this.toolInstance.destroy)) {
    this.toolInstance.destroy();
  }
}

However, in the core collection manager src/components/blocks.ts, block.destroy() is never called anywhere:

1. When removing a block:

// src/components/blocks.ts: Lines 285-295
public remove(index: number): void {
  if (isNaN(index)) {
    index = this.length - 1;
  }

  this.blocks[index].holder.remove();
  this.blocks[index].call(BlockToolAPI.REMOVED); // <-- Calls removed hook, BUT NEVER destroy()!
  this.blocks.splice(index, 1);
}

2. When clearing all blocks (removeAll / blocks.clear()):

// src/components/blocks.ts: Lines 300-306
public removeAll(): void {
  this.workingArea.innerHTML = '';
  this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED)); // <-- NEVER destroy()!
  this.blocks.length = 0;
}

3. When updating a block (replace / blocks.update()):

// src/components/blocks.ts: Lines 230-240
public replace(index: number, block: Block): void {
  const prevBlock = this.blocks[index];
  prevBlock.holder.replaceWith(block.holder);
  this.blocks[index] = block; // <-- Neither removed NOR destroy() is called!
}

4. When inserting with replace (insert(..., replace = true)):

// src/components/blocks.ts: Lines 200-204
if (replace) {
  this.blocks[index].holder.remove();
  this.blocks[index].call(BlockToolAPI.REMOVED); // <-- destroy() skipped!
}

πŸ’₯ Real-World Impact

  1. Leaked MutationObservers: this.unwatchBlockMutations() is never executed. Each deleted block leaves an active MutationObserver observing an orphaned DOM node.
  2. Leaked Input Event Listeners: this.removeInputEvents() is never executed; all keydown, paste, and input handlers remain bound to inputs.
  3. Broken Tool Teardown: Third-party plugins that implement destroy() (e.g., stopping video streams, tearing down WebGL/Three.js canvases, clearing setInterval timers, closing WebSocket channels) never have their destroy() hook fired when a block is deleted or updated!
  4. Over a long editing session or dynamic updates in collaborative editors (e.g., Yjs), hundreds of orphaned blocks accumulate in memory.

πŸ’‘ Proposed Solution & Patch

src/components/blocks.ts

@@ -201,6 +201,7 @@ export default class Blocks {
     if (replace) {
       this.blocks[index].holder.remove();
       this.blocks[index].call(BlockToolAPI.REMOVED);
+      this.blocks[index].destroy();
     }

     const deleteCount = replace ? 1 : 0;
@@ -236,6 +237,8 @@ export default class Blocks {
     const prevBlock = this.blocks[index];

     prevBlock.holder.replaceWith(block.holder);
+    prevBlock.call(BlockToolAPI.REMOVED);
+    prevBlock.destroy();

     this.blocks[index] = block;
   }
@@ -290,6 +293,7 @@ export default class Blocks {
     this.blocks[index].holder.remove();

     this.blocks[index].call(BlockToolAPI.REMOVED);
+    this.blocks[index].destroy();

     this.blocks.splice(index, 1);
   }
@@ -301,6 +305,9 @@ export default class Blocks {
     this.workingArea.innerHTML = '';

-    this.blocks.forEach((block) => block.call(BlockToolAPI.REMOVED));
+    this.blocks.forEach((block) => {
+      block.call(BlockToolAPI.REMOVED);
+      block.destroy();
+    });

     this.blocks.length = 0;
   }

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions