Real-time progress updates for code analysis and evolution.

Status note. This page was written against an earlier version of the API and has not been fully re-verified. Event names and payload shapes may have changed. Treat it as a guide rather than a contract, and expect updates.

Overview

Code Evolution Lab uses Server-Sent Events (SSE) to stream real-time updates during code analysis and solution evolution. This provides immediate feedback without polling.

SSE vs WebSocket

Feature SSE WebSocket
Direction Server → Client only Bidirectional
Protocol HTTP WS/WSS
Reconnection Automatic Manual
Complexity Simple More complex

SSE is ideal for our use case since we only need server-to-client updates.

Endpoints

GET /api/sse/evolution/:analysisId

Stream evolution progress for an analysis.

Authentication: Required (via query parameter)

URL:

/api/sse/evolution/{analysisId}?token={accessToken}

Headers:

Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Event Types

quick-solutions

Sent when heuristic solutions are ready (fast, before evolution).

The phase field is normally heuristic. It is fallback when the primary generator threw and the generic generator produced the solutions instead.

event: quick-solutions
data: {
  "issueId": "N+1 Query in getOrders",
  "issueType": "n_plus_1_query",
  "phase": "heuristic",
  "solutions": [
    {
      "id": "sol-1",
      "rank": 1,
      "type": "batch-query-before-loop",
      "fitnessScore": 72.5,
      "code": "..."
    }
  ]
}

evolution-start

Sent when evolutionary refinement begins.

event: evolution-start
data: {
  "issueId": "N+1 Query in getOrders",
  "issueType": "n_plus_1_query"
}

evolution-progress

Sent after each generation of evolution.

event: evolution-progress
data: {
  "issueId": "N+1 Query in getOrders",
  "issueType": "n_plus_1_query",
  "issueTitle": "N+1 Query in getOrders",
  "generation": 3,
  "maxGenerations": 10,
  "bestFitness": 81.3,
  "avgFitness": 71.2,
  "bestSolution": {
    "code": "...",
    "fitness": 81.3,
    "mutations": ["addOptimization"]
  },
  "population": [...]
}

evolution-complete

Sent when evolution finishes successfully.

event: evolution-complete
data: {
  "issueId": "N+1 Query in getOrders",
  "issueType": "n_plus_1_query",
  "phase": "evolutionary",
  "solutions": [
    {
      "id": "sol-evolved-1",
      "rank": 1,
      "type": "batch-query-before-loop",
      "fitnessScore": 85.2,
      "code": "...",
      "generationMethod": "evolutionary",
      "generationsUsed": 7
    }
  ]
}

solutions-complete

Sent instead of the evolution events when an issue does not qualify for evolution. Evolution runs only for issue types n_plus_1_query, nested_loops, nested_array_methods and await_in_loop, or for any issue of high or critical severity — everything else finishes after the heuristic phase and emits this event.

A client must handle it, or issues that stop at the heuristic phase will appear to hang: no evolution-start, no evolution-complete, nothing further for that issue.

event: solutions-complete
data: {
  "issueId": "Unused import in utils.ts",
  "issueType": "unused_import",
  "phase": "heuristic-only",
  "solutions": [...]
}

evolution-timeout

Sent if evolution times out (falls back to heuristic solutions).

event: evolution-timeout
data: {
  "issueId": "N+1 Query in getOrders",
  "issueType": "n_plus_1_query",
  "fallbackSolutions": [...]
}

error

Sent if an error occurs.

event: error
data: {
  "message": "Analysis not found",
  "code": "NOT_FOUND"
}

done

Sent when all processing is complete.

event: done
data: {
  "message": "Analysis complete"
}

Server Implementation

// src/api/routes/sse.routes.ts
import { Router } from 'express';

const router = Router();

router.get('/evolution/:analysisId', async (req, res) => {
  const { analysisId } = req.params;
  const { token } = req.query;
  
  // Set SSE headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  
  // Verify token
  try {
    const payload = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
    req.user = payload;
  } catch (error) {
    res.write(`event: error\ndata: {"message": "Invalid token"}\n\n`);
    return res.end();
  }
  
  // Create analyzer and listen for events
  const analyzer = new CodeAnalyzer();
  
  analyzer.on('quick-solutions', (data) => {
    res.write(`event: quick-solutions\ndata: ${JSON.stringify(data)}\n\n`);
  });
  
  analyzer.on('evolution-start', (data) => {
    res.write(`event: evolution-start\ndata: ${JSON.stringify(data)}\n\n`);
  });
  
  analyzer.on('evolution-progress', (data) => {
    res.write(`event: evolution-progress\ndata: ${JSON.stringify(data)}\n\n`);
  });
  
  analyzer.on('evolution-complete', (data) => {
    res.write(`event: evolution-complete\ndata: ${JSON.stringify(data)}\n\n`);
  });
  
  analyzer.on('solutions-complete', (data) => {
    res.write(`event: solutions-complete\ndata: ${JSON.stringify(data)}\n\n`);
  });
  
  analyzer.on('evolution-timeout', (data) => {
    res.write(`event: evolution-timeout\ndata: ${JSON.stringify(data)}\n\n`);
  });
  
  // Handle client disconnect
  req.on('close', () => {
    analyzer.removeAllListeners();
  });
  
  // Start analysis
  try {
    await analyzer.analyzeCode(code, filePath, true);
    res.write(`event: done\ndata: {"message": "Analysis complete"}\n\n`);
  } catch (error) {
    res.write(`event: error\ndata: {"message": "${error.message}"}\n\n`);
  }
  
  res.end();
});

Client Implementation

JavaScript/TypeScript

class SSEClient {
  private eventSource: EventSource | null = null;
  
  connect(analysisId: string, token: string): Observable<SSEEvent> {
    return new Observable(observer => {
      const url = `${API_URL}/sse/evolution/${analysisId}?token=${token}`;
      this.eventSource = new EventSource(url);
      
      // Handle specific events
      this.eventSource.addEventListener('quick-solutions', (e) => {
        observer.next({ type: 'quick-solutions', data: JSON.parse(e.data) });
      });
      
      this.eventSource.addEventListener('evolution-progress', (e) => {
        observer.next({ type: 'evolution-progress', data: JSON.parse(e.data) });
      });
      
      this.eventSource.addEventListener('evolution-complete', (e) => {
        observer.next({ type: 'evolution-complete', data: JSON.parse(e.data) });
      });
      
      // Issues that don't qualify for evolution end here instead. Without
      // this listener they appear to hang after quick-solutions.
      this.eventSource.addEventListener('solutions-complete', (e) => {
        observer.next({ type: 'solutions-complete', data: JSON.parse(e.data) });
      });
      
      this.eventSource.addEventListener('evolution-timeout', (e) => {
        observer.next({ type: 'evolution-timeout', data: JSON.parse(e.data) });
      });
      
      this.eventSource.addEventListener('done', (e) => {
        observer.complete();
        this.disconnect();
      });
      
      this.eventSource.addEventListener('error', (e) => {
        observer.error(e);
        this.disconnect();
      });
      
      // Cleanup on unsubscribe
      return () => this.disconnect();
    });
  }
  
  disconnect() {
    if (this.eventSource) {
      this.eventSource.close();
      this.eventSource = null;
    }
  }
}

Angular Service

@Injectable({ providedIn: 'root' })
export class AnalysisService {
  private sseClient = new SSEClient();
  
  analyzeWithProgress(code: string): Observable<AnalysisProgress> {
    return new Observable(observer => {
      // First, submit the analysis
      this.http.post<{ analysisId: string }>('/api/analyze', { code })
        .subscribe({
          next: (response) => {
            // Connect to SSE for progress
            this.sseClient.connect(response.analysisId, this.auth.getToken())
              .subscribe({
                next: (event) => observer.next(this.mapEvent(event)),
                error: (err) => observer.error(err),
                complete: () => observer.complete()
              });
          },
          error: (err) => observer.error(err)
        });
    });
  }
  
  private mapEvent(event: SSEEvent): AnalysisProgress {
    switch (event.type) {
      case 'quick-solutions':
        return { phase: 'heuristic', solutions: event.data.solutions };
      case 'evolution-progress':
        return { 
          phase: 'evolution', 
          generation: event.data.generation,
          maxGenerations: event.data.maxGenerations,
          bestFitness: event.data.bestFitness
        };
      case 'evolution-complete':
        return { phase: 'complete', solutions: event.data.solutions };
      case 'solutions-complete':
        // Heuristic-only issue: final, even though evolution never ran
        return { phase: 'complete', solutions: event.data.solutions };
      case 'evolution-timeout':
        // Also final — fall back to the heuristic solutions
        return { phase: 'complete', solutions: event.data.fallbackSolutions };
      default:
        return { phase: 'unknown' };
    }
  }
}

Angular Component

@Component({
  template: `
    <div class="analysis-progress">
      @if (progress().phase === 'heuristic') {
        <p>Found {{ progress().solutions?.length }} quick solutions</p>
      }
      
      @if (progress().phase === 'evolution') {
        <p>Evolving solutions...</p>
        <progress 
          [value]="progress().generation" 
          [max]="progress().maxGenerations">
        </progress>
        <p>Generation {{ progress().generation }}/{{ progress().maxGenerations }}</p>
        <p>Best fitness: {{ progress().bestFitness | number:'1.1-1' }}</p>
      }
      
      @if (progress().phase === 'complete') {
        <p>Analysis complete!</p>
        <solution-list [solutions]="progress().solutions" />
      }
    </div>
  `
})
export class EvolutionProgressComponent {
  progress = signal<AnalysisProgress>({ phase: 'starting' });
  
  constructor(private analysisService: AnalysisService) {}
  
  startAnalysis(code: string) {
    this.analysisService.analyzeWithProgress(code).subscribe({
      next: (progress) => this.progress.set(progress),
      error: (err) => console.error('Analysis failed:', err)
    });
  }
}

Event Flow Diagram

Client                          Server
  │                               │
  │── POST /api/analyze ─────────►│
  │◄── { analysisId } ────────────│
  │                               │
  │── GET /sse/evolution/{id} ───►│
  │                               │
  │◄── event: quick-solutions ────│ (immediate)
  │                               │
  │◄── event: evolution-start ────│
  │                               │
  │◄── event: evolution-progress ─│ (gen 1)
  │◄── event: evolution-progress ─│ (gen 2)
  │◄── event: evolution-progress ─│ (gen 3)
  │        ...                    │
  │◄── event: evolution-complete ─│
  │                               │
  │◄── event: done ───────────────│
  │                               │
  │── (connection closed) ────────│

Error Handling

Connection Errors

this.eventSource.onerror = (error) => {
  if (this.eventSource?.readyState === EventSource.CLOSED) {
    // Connection was closed - may want to reconnect
    console.log('SSE connection closed');
  } else {
    // Error occurred
    console.error('SSE error:', error);
  }
};

Automatic Reconnection

EventSource automatically reconnects on connection loss. To disable:

this.eventSource.onerror = () => {
  this.eventSource?.close();  // Prevent reconnection
};

Timeout Handling

const timeout = setTimeout(() => {
  this.eventSource?.close();
  observer.error(new Error('SSE timeout'));
}, 60000);  // 60 second timeout

this.eventSource.addEventListener('done', () => {
  clearTimeout(timeout);
});

Best Practices

  1. Always close connections when component unmounts
  2. Handle all event types including errors
  3. Set appropriate timeouts for long operations
  4. Show progress UI during evolution
  5. Fall back gracefully on timeout/error

Debugging

Browser DevTools

  1. Open Network tab
  2. Filter by “EventStream”
  3. Click on SSE connection
  4. View “EventStream” tab for messages

Server Logging

analyzer.on('evolution-progress', (data) => {
  console.log(`[SSE] Gen ${data.generation}: fitness=${data.bestFitness}`);
  res.write(`event: evolution-progress\ndata: ${JSON.stringify(data)}\n\n`);
});