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
- Leaked
MutationObservers: this.unwatchBlockMutations() is never executed. Each deleted block leaves an active MutationObserver observing an orphaned DOM node.
- Leaked Input Event Listeners:
this.removeInputEvents() is never executed; all keydown, paste, and input handlers remain bound to inputs.
- 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!
- 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;
}
1. Complete Omission of
block.destroy()inBlocksCollectionπ 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
Blockclass defines an essential cleanup method insrc/components/block/index.ts:However, in the core collection manager
src/components/blocks.ts,block.destroy()is never called anywhere:1. When removing a block:
2. When clearing all blocks (
removeAll/blocks.clear()):3. When updating a block (
replace/blocks.update()):4. When inserting with replace (
insert(..., replace = true)):π₯ Real-World Impact
MutationObservers:this.unwatchBlockMutations()is never executed. Each deleted block leaves an activeMutationObserverobserving an orphaned DOM node.this.removeInputEvents()is never executed; all keydown, paste, and input handlers remain bound to inputs.destroy()(e.g., stopping video streams, tearing down WebGL/Three.js canvases, clearingsetIntervaltimers, closing WebSocket channels) never have theirdestroy()hook fired when a block is deleted or updated!π‘ Proposed Solution & Patch
src/components/blocks.ts