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:

  1. Flash Persistence updates (synchronous)
  2. Angular effect() synchronization (asynchronous)
  3. 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

  1. Asynchronous: Scheduled, not immediate
  2. Change Detection Dependent: Runs when Angular checks for changes
  3. Batched: Multiple updates might be batched together
  4. 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)

  1. ✅ saveCurrentFlow() - Now saves actual current state
  2. ✅ executeCurrentFlow() - Executes actual current flow
  3. ✅ executeFlowFromTester() - Tests actual current flow
  4. ✅ createNewFlow() - Clears actual state

What This Means


✅ 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: