TROUBLESHOOTING RACE CONDITION
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
Troubleshooting: Race Condition - Persistence Broken Again
Status: ✅ FIXED
Date: October 23, 2025
Issue: Deleted blocks return after refresh even after auto-save fix
🐛 The Problem
Even after implementing auto-save on block deletion, blocks were still coming back after page refresh.
Symptoms
1. Delete block → Console shows "💾 Flow auto-saved after block deletion"
2. Refresh page
3. Block is back! ❌
4. Flow loads with original 2 blocks instead of 1
Logs Analysis
// BEFORE deletion:
logic-builder.component.ts:2717 📥 Loading flow: menso flow with 2 blocks
// AFTER deletion:
singularity.service.ts:350 ✅ Deleted 1 blocks via Flash Persistence
logic-builder.component.ts:1509 🗑️ Deleted block: block_xyz
logic-builder.component.ts:348 🔄 Received 1 blocks from Flash Persistence ← Flash has 1 block ✅
logic-builder.component.ts:2650 ✅ Flow saved: menso flow (flow_1761254148303)
logic-builder.component.ts:1514 💾 Flow auto-saved after block deletion
// AFTER refresh:
logic-builder.component.ts:2717 📥 Loading flow: menso flow with 2 blocks ← STILL 2 BLOCKS! ❌
🔍 Root Cause: Race Condition
The issue was a timing race condition between:
- Flash Persistence updates (synchronous)
- Angular
effect()synchronization (asynchronous) - Flow saving (reads from local signals)
The Race Condition Explained
Two State Layers
// Layer 1: Flash Persistence (Service) - Single Source of Truth
singularityService.blocks() // 1 block after deletion
// Layer 2: Local Component Signals - UI Mirror
canvasBlocks() // Still 2 blocks (effect hasn't run yet)
The Effect Synchronization
// Constructor effect - Syncs Flash Persistence → Local Signal
constructor() {
effect(() => {
const blocks = this.singularityService.blocks(); // Read from Flash Persistence
this.canvasBlocks.set(blocks); // Update local signal
});
}
Key Point: The effect() runs asynchronously - it's scheduled by Angular's change detection, not executed immediately.
Timeline of the Bug
Timeline:
1. User deletes block (block_xyz)
└─ deleteBlock() called
2. singularityService.deleteBlocks([blockId])
└─ Flash Persistence updated: 1 block ✅
└─ effect() SCHEDULED (not run yet)
3. await saveCurrentFlow()
└─ Reads: const blocks = this.canvasBlocks()
└─ canvasBlocks still has 2 blocks ❌
└─ Saves flow with 2 blocks to storage
4. effect() finally runs
└─ canvasBlocks synced to 1 block
└─ TOO LATE - flow already saved with 2 blocks
5. User refreshes page
└─ Loads flow from storage
└─ Flow has 2 blocks (from step 3)
└─ Deleted block is back!
Visual Diagram
┌──────────────────────────────────────────────────────┐
│ TIME: T0 - User deletes block │
│ │
│ Flash Persistence: [block1, block2] → [block1] ✅ │
│ Local Signal: [block1, block2] ← NOT SYNCED │
│ effect() status: SCHEDULED (not run) │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ TIME: T1 - saveCurrentFlow() executes │
│ │
│ Reads: canvasBlocks() → [block1, block2] ❌ │
│ Saves flow to storage with 2 blocks │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ TIME: T2 - effect() finally runs │
│ │
│ Local Signal synced: [block1] ✅ │
│ But flow already saved with 2 blocks ❌ │
└──────────────────────────────────────────────────────┘
✅ The Fix: Read from Single Source of Truth
Principle: Always read from Flash Persistence (service), never from local component signals.
Before (BROKEN) - Multiple Methods Affected
1. saveCurrentFlow() ❌
async saveCurrentFlow(): Promise<void> {
// ❌ PROBLEM: Reads from local signal (might be stale)
const blocks = this.canvasBlocks();
const connections = this.canvasConnections();
const flow = { id, name, blocks, connections };
await this.storage.put(flow.id, flow); // Saves stale data!
}
2. executeCurrentFlow() ❌
async executeCurrentFlow(): Promise<void> {
// ❌ PROBLEM: Reads from local signal
const blocks = this.canvasBlocks();
const connections = this.canvasConnections();
const flow = { id, name, blocks, connections };
await this.executionEngine.executeFlow(flow.id); // Executes stale flow!
}
3. executeFlowFromTester() ❌
async executeFlowFromTester(variables: Map<string, any>): Promise<void> {
// ❌ PROBLEM: Reads from local signal
const blocks = this.canvasBlocks();
const connections = this.canvasConnections();
const flow = { id, name, blocks, connections };
// Executes stale flow!
}
4. createNewFlow() ❌
createNewFlow(): void {
// ❌ PROBLEM: Updates local signals only
this.canvasBlocks.set([]);
this.canvasConnections.set([]);
// Flash Persistence not updated!
}
After (FIXED) - All Methods Use Service
1. saveCurrentFlow() ✅
async saveCurrentFlow(): Promise<void> {
// ✅ FIX: Read from service (Flash Persistence - always current)
const blocks = this.singularityService.blocks();
const connections = this.singularityService.connections();
const flow = { id, name, blocks, connections };
await this.storage.put(flow.id, flow); // Saves CURRENT data!
console.log(` - Blocks saved: ${blocks.length}`); // Now accurate
}
2. executeCurrentFlow() ✅
async executeCurrentFlow(): Promise<void> {
// ✅ FIX: Read from service
const blocks = this.singularityService.blocks();
const connections = this.singularityService.connections();
const flow = { id, name, blocks, connections };
console.log(`⚡ Executing flow with ${blocks.length} blocks`); // Accurate
await this.executionEngine.executeFlow(flow.id);
}
3. executeFlowFromTester() ✅
async executeFlowFromTester(variables: Map<string, any>): Promise<void> {
// ✅ FIX: Read from service
const blocks = this.singularityService.blocks();
const connections = this.singularityService.connections();
console.log(`🧪 Testing flow with ${blocks.length} blocks`); // Accurate
const flow = { id, name, blocks, connections };
// Executes CURRENT flow!
}
4. createNewFlow() ✅
createNewFlow(): void {
// ✅ FIX: Update service (Flash Persistence), not local signals
this.singularityService.setBlocks([]);
this.singularityService.setConnections([]);
console.log('📄 New flow created - canvas cleared');
}
🎯 The Pattern: Single Source of Truth
❌ ANTI-PATTERN: Multiple Sources of Truth
Flash Persistence (Service) ←─────┐
│ RACE CONDITION
Local Signals (Component) ←─────┘
↓
Code reads from local signals → STALE DATA
✅ CORRECT PATTERN: Single Source of Truth
Flash Persistence (Service) ← SINGLE SOURCE OF TRUTH
↓
Local Signals (Component) ← MIRROR ONLY (for UI)
↓
Code reads from service → ALWAYS CURRENT
Rules to Follow
1. ALWAYS Read from Service
// ✅ DO THIS
const blocks = this.singularityService.blocks();
const connections = this.singularityService.connections();
// ❌ NEVER DO THIS (in operations that need current state)
const blocks = this.canvasBlocks(); // Might be stale!
const connections = this.canvasConnections(); // Might be stale!
2. ALWAYS Write to Service
// ✅ DO THIS
this.singularityService.setBlocks([]);
this.singularityService.deleteBlocks([blockId]);
this.singularityService.addBlock(block);
// ❌ NEVER DO THIS
this.canvasBlocks.set([]); // Only updates local signal!
3. Use Local Signals for UI Only
// ✅ OK for template binding
<div *ngFor="let block of canvasBlocks()">...</div>
// ✅ OK for UI state
if (this.canvasBlocks().length === 0) {
this.showEmptyState = true; // Just UI feedback
}
// ❌ NOT OK for data operations
const flow = {
blocks: this.canvasBlocks(), // DON'T use in save/execute
connections: this.canvasConnections()
};
🔬 Why Effect() Can't Be Relied Upon
Effect() Characteristics
- Asynchronous: Scheduled, not immediate
- Change Detection Dependent: Runs when Angular checks for changes
- Batched: Multiple updates might be batched together
- Not Guaranteed Timing: Can't predict exactly when it runs
Example Timeline
console.log('1. Before delete');
singularityService.deleteBlocks([id]); // Updates Flash Persistence
console.log('2. After delete - effect NOT run yet');
const blocks = canvasBlocks(); // Still old value!
console.log('3. Blocks:', blocks.length); // Still 2!
// Later (unpredictable timing):
// effect() runs → canvasBlocks synced
Why This Happens
// When you call:
singularityService.deleteBlocks([id]);
// This happens:
// 1. Signal updated: _blocks.set([...]) ← Immediate
// 2. Angular schedules effect(): effect(() => ...) ← Scheduled
// 3. Your code continues: const blocks = canvasBlocks() ← Immediate
// 4. Effect runs: this.canvasBlocks.set(...) ← Later!
📊 Impact
Fixed Methods (4 total)
- ✅
saveCurrentFlow()- Now saves actual current state - ✅
executeCurrentFlow()- Executes actual current flow - ✅
executeFlowFromTester()- Tests actual current flow - ✅
createNewFlow()- Clears actual state
What This Means
- ✅ Block deletions persist across refreshes
- ✅ Block additions persist correctly
- ✅ Flow executions use current state
- ✅ Tests use current state
- ✅ No more race conditions
- ✅ Perfect data consistency
✅ Verification
After the fix, you should see in logs:
Delete Block
🗑️ Deleted block: block_xyz
✅ Flow saved: menso flow (flow_1761254148303)
- Blocks saved: 1 ← CORRECT COUNT!
💾 Flow auto-saved after block deletion
Refresh Page
📥 Loading flow: menso flow with 1 blocks ← CORRECT!
✅ Flow loaded: menso flow (flow_1761254148303)
- Blocks: 1 ← STAYS DELETED!
Execute Flow
⚡ Executing flow: menso flow with 1 blocks ← CURRENT STATE!
🎉 Summary
Root Cause: Race condition between Flash Persistence updates and Angular effect() synchronization
Fix: Always read from singularityService.blocks() and singularityService.connections() (single source of truth) instead of local component signals
Result: Perfect data consistency, no race conditions, all operations use current state
Principle:
- Flash Persistence = Single Source of Truth (READ FROM HERE)
- Local Signals = UI Mirror (DISPLAY ONLY)