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
- Always close connections when component unmounts
- Handle all event types including errors
- Set appropriate timeouts for long operations
- Show progress UI during evolution
- Fall back gracefully on timeout/error
Debugging
Browser DevTools
- Open Network tab
- Filter by “EventStream”
- Click on SSE connection
- 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`);
});