8.1 KiB
BUG-2: Blockly Node Randomly Deleted on Tab Close
Priority: P0 - Data loss risk
Status: 🔴 Research
Introduced in: Phase 3 Task 12 (Blockly integration)
Symptoms
- Add a Logic Builder (Blockly) node to canvas ✅
- Open the Blockly editor tab (click "Edit Logic Blocks") ✅
- Add some blocks in the Blockly editor ✅
- Close the Blockly editor tab ✅
- SOMETIMES the Logic Builder node disappears from canvas ❌
Frequency: Intermittent - doesn't happen every time (need to determine success rate)
User Impact
- Severity: Critical - Data loss
- Frequency: Intermittent (need testing to determine %)
- Frustration: Extremely high - losing work is unacceptable
- Workaround: None - just have to be careful and check after closing
Initial Hypotheses
Hypothesis 1: Race Condition in Save/Close
When closing tab, workspace might not be saved before close event completes:
- User clicks close button
- Tab starts closing
- Workspace save triggered but async
- Tab closes before save completes
- Some cleanup logic runs
- Node gets deleted?
Hypothesis 2: Event Bubbling to Canvas
Close button click might bubble through to canvas:
- Click close button on tab
- Event bubbles to canvas layer
- Canvas interprets as "click empty space"
- Triggers deselect
- Some condition causes node deletion instead of just deselection
Hypothesis 3: Keyboard Shortcut Conflict
Accidental Delete key press during close:
- Tab is closing
- User presses Delete (or Esc triggers something)
- Node is selected in background
- Delete key removes node
Hypothesis 4: Node Lifecycle Cleanup Bug
Tab close triggers node cleanup by mistake:
- Tab close event fires
- Cleanup logic runs to remove tab from state
- Logic accidentally also removes associated node
- Node deleted from graph
Investigation Tasks
Step 1: Reproduce Consistently
- Test closing tab 20 times, track success vs failure
- Try different timing (close immediately vs wait a few seconds)
- Try with empty workspace vs with blocks
- Try with multiple Blockly nodes
- Check if it happens on first close vs subsequent closes
Step 2: Add Logging
Add comprehensive logging to trace node lifecycle:
// In CanvasTabs.tsx - tab close handler
console.log('[CanvasTabs] Closing Blockly tab for node:', nodeId);
// In nodegrapheditor.ts - node deletion
console.log('[NodeGraphEditor] Node being deleted:', nodeId, 'Reason:', reason);
// In logic-builder.js runtime node
console.log('[LogicBuilder] Node lifecycle event:', event, nodeId);
Step 3: Check Workspace Save Timing
- Verify
handleBlocklyWorkspaceChangeis called before close - Add timing logs to see save vs close race
- Check if workspace parameter is actually saved to node model
Step 4: Event Flow Analysis
- Trace all events fired during tab close
- Check if any events reach canvas
- Look for stopPropagation calls
Step 5: Review Cleanup Logic
- Check
CanvasTabsContextcleanup on unmount - Review node selection state during close
- Look for any "remove node if X condition" logic
Files to Investigate
-
packages/noodl-editor/src/editor/src/views/CanvasTabs/CanvasTabs.tsx- Tab close handler
- Workspace change handler
- Event propagation
-
packages/noodl-editor/src/editor/src/contexts/CanvasTabsContext.tsx- Tab state management
- Cleanup logic
- Node ID mapping
-
packages/noodl-editor/src/editor/src/views/nodegrapheditor.ts- Node deletion logic
- Selection state during tab operations
- Event handlers that might trigger deletion
-
packages/noodl-runtime/src/nodes/std-library/logic-builder.js- Runtime node lifecycle
- Parameter update handlers
- Any cleanup logic
-
packages/noodl-editor/src/editor/src/views/BlocklyEditor/BlocklyWorkspace.tsx- Workspace save logic
- Component unmount/cleanup
Proposed Solutions (Pending Investigation)
Solution A: Ensure Save Before Close
const handleCloseTab = async (nodeId: string) => {
console.log('[CanvasTabs] Closing tab for node:', nodeId);
// Save workspace first
await saveWorkspace(nodeId);
// Then close tab
removeTab(nodeId);
};
Solution B: Add Confirmation for Unsaved Changes
const handleCloseTab = (nodeId: string) => {
if (hasUnsavedChanges(nodeId)) {
// Show confirmation dialog
showConfirmDialog({
message: 'Close without saving changes?',
onConfirm: () => removeTab(nodeId)
});
} else {
removeTab(nodeId);
}
};
Solution C: Prevent Event Bubbling
const handleCloseClick = (e: React.MouseEvent, nodeId: string) => {
e.stopPropagation(); // Prevent bubbling to canvas
e.preventDefault();
closeTab(nodeId);
};
Solution D: Guard Against Accidental Deletion
// In node deletion logic
const deleteNode = (nodeId: string, source: string) => {
// Don't delete if associated Blockly tab is open
if (blocklyTabOpenForNode(nodeId)) {
console.warn('[NodeGraphEditor] Prevented deletion of node with open Blockly tab');
return;
}
// Proceed with deletion
actuallyDeleteNode(nodeId);
};
Testing Plan
Reproduction Testing
- Create Logic Builder node
- Open editor, add blocks, close tab
- Repeat 20 times, track failures
- Try different scenarios (empty, with blocks, multiple nodes)
- Document exact conditions when it fails
With Logging
- Add comprehensive logging
- Reproduce the bug with logs active
- Analyze log sequence to find root cause
- Identify exact point where deletion occurs
After Fix
- Test tab close 50 times - should NEVER delete node
- Test with multiple Blockly nodes open
- Test rapid open/close cycles
- Test with unsaved changes
- Test with saved changes
- Verify workspace is properly saved on close
Success Criteria
- Can close Blockly tab 100 times without a single node deletion
- Workspace is always saved before tab closes
- No event bubbling causes unintended canvas clicks
- No race conditions between save and close
- Logging shows clean lifecycle with no errors
Related Issues
This might be related to:
- Tab state management from Phase 3 Task 12
- Node selection state management
- Canvas event handling
Investigation Findings (January 16, 2026)
What I Checked
LogicBuilder.AllTabsClosedhandler - Just shows the canvas, NOT deleting nodes ✅CanvasTabsContext.closeTab()- Only removes tab from state, doesn't touch nodes ✅- Event propagation - Tab close button uses
e.stopPropagation()✅
Most Likely Culprits
Based on the code review, the most likely causes are:
1. Keyboard Focus + Delete Key
When the Blockly tab closes, focus may return to the canvas with the node still selected. If Delete/Backspace is pressed (or held down from Blockly editing), it could trigger node deletion.
Test: After closing Blockly tab, check if the node is still selected. Try pressing Delete immediately after closing.
2. clearDeleteModeTimer in nodegrapheditor.ts (line 185)
There's a timer for "delete mode" in the node graph editor. This could be related to delayed deletion behavior.
3. Race Condition with Canvas Visibility
When LogicBuilder.AllTabsClosed triggers setCanvasVisibility(true), the canvas re-renders. If the node was selected before opening Blockly, and some keyboard event fires during the transition, it could trigger deletion.
Recommended Debug Steps
- Add console logging to the Delete key handler to see when it fires
- Log node selection state when closing Blockly tab
- Log any pending timers (clearDeleteModeTimer)
- Check if node exists BEFORE and AFTER
LogicBuilder.AllTabsClosedevent
Potential Quick Fix
Add a guard in the delete handler to ignore deletion when a Blockly tab was just closed:
// In delete handler
if (Date.now() - lastBlocklyTabCloseTime < 100) {
console.warn('[NodeGraphEditor] Ignoring delete during Blockly tab close transition');
return;
}
Last Updated: January 16, 2026