- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.5 KiB
9.5 KiB
SSE Implementation Guide
Quick Start
This guide will help you implement and test the SSE-based session architecture.
Step 1: Integration with server.js
Add these lines to your existing /home/uroma/obsidian-web-interface/server.js:
// After existing imports (around line 12)
const eventBus = require('./services/event-bus');
const sseManager = require('./services/sse-manager');
const sessionRoutes = require('./routes/session-routes');
const sseRoutes = require('./routes/sse-routes');
// After existing middleware (around line 270)
// Register new API routes
app.use('/api', sessionRoutes);
app.use('/api', sseRoutes);
// Add monitoring endpoint
app.get('/api/debug/metrics', (req, res) => {
res.json({
eventBus: eventBus.getMetrics(),
sse: sseManager.getStats(),
timestamp: Date.now()
});
});
// Update graceful shutdown (around line 2600)
const originalCleanup = /* existing cleanup logic or null */;
process.on('SIGTERM', async () => {
console.log('[Server] Starting graceful shutdown...');
// Cleanup SSE connections
sseManager.cleanup();
// ... existing cleanup ...
process.exit(0);
});
Step 2: Modify ClaudeService to emit events
In /home/uroma/obsidian-web-interface/services/claude-service.js:
// Add at top of file
const eventBus = require('./event-bus');
// Find the section where Claude output is handled
// Replace callback-based approach with EventBus emits
// Example: When output is received
handleSessionOutput(sessionId, output) {
// Old way:
// this.emit('session-output', { sessionId, output });
// New way:
eventBus.emit('session-output', {
sessionId,
type: 'stdout',
content: output,
timestamp: Date.now()
});
}
// Example: When operations are detected
handleOperationsDetected(sessionId, operations, response) {
eventBus.emit('operations-detected', {
sessionId,
operations,
response: response.substring(0, 500) + '...'
});
}
Step 3: Test SSE Endpoint
Test 1: Basic SSE Connection
# Terminal 1: Start server
cd /home/uroma/obsidian-web-interface
npm start
# Terminal 2: Create a test session
curl -X POST http://localhost:3010/claude/api/claude/sessions \
-H "Content-Type: application/json" \
-d '{"workingDir":"/home/uroma"}'
# Note the session ID from response
# Terminal 3: Test SSE connection (replace SESSION_ID)
curl -N http://localhost:3010/api/session/SESSION_ID/events
Test 2: SSE with curl (watch events)
# Connect to SSE and watch for events
curl -N -H "Accept: text/event-stream" \
http://localhost:3010/api/session/SESSION_ID/events
Test 3: Send command via REST API
# Send a command
curl -X POST http://localhost:3010/api/session/SESSION_ID/prompt \
-H "Content-Type: application/json" \
-d '{"command":"ls -la"}'
# Watch for output in your SSE connection (Test 2)
Step 4: Browser Testing
Create a test HTML file /home/uroma/obsidian-web-interface/public/test-sse.html:
<!DOCTYPE html>
<html>
<head>
<title>SSE Test</title>
<style>
body { font-family: monospace; padding: 20px; }
#events { max-height: 500px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
.event { margin: 5px 0; padding: 5px; background: #f0f0f0; }
.event.error { background: #ffcccc; }
.event.output { background: #ccffcc; }
</style>
</head>
<body>
<h1>SSE Test</h1>
<div>
<label>Session ID:</label>
<input type="text" id="sessionId" placeholder="session-123" size="40">
<button onclick="connect()">Connect</button>
<button onclick="disconnect()">Disconnect</button>
<button onclick="clearEvents()">Clear</button>
</div>
<div>
<label>Command:</label>
<input type="text" id="command" placeholder="echo hello" size="40">
<button onclick="sendCommand()">Send</button>
</div>
<div id="events"></div>
<script src="/js/sse-client.js"></script>
<script>
let eventStream = null;
function connect() {
const sessionId = document.getElementById('sessionId').value;
if (!sessionId) {
alert('Please enter a session ID');
return;
}
eventStream = new SessionEventStream(sessionId, {
reconnectInterval: 2000,
maxReconnectAttempts: 20
});
eventStream.on('connected', (data) => {
logEvent('connected', data);
});
eventStream.on('output', (data) => {
logEvent('output', data);
});
eventStream.on('error', (data) => {
logEvent('error', data, true);
});
eventStream.on('status', (data) => {
logEvent('status', data);
});
eventStream.on('reconnecting', (data) => {
logEvent('reconnecting', data, true);
});
eventStream.on('disconnected', (data) => {
logEvent('disconnected', data, true);
});
}
function disconnect() {
if (eventStream) {
eventStream.disconnect();
eventStream = null;
}
}
async function sendCommand() {
const sessionId = document.getElementById('sessionId').value;
const command = document.getElementById('command').value;
if (!sessionId || !command) {
alert('Please enter session ID and command');
return;
}
try {
const response = await fetch(`/api/session/${sessionId}/prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
const result = await response.json();
logEvent('command-sent', result);
} catch (error) {
logEvent('fetch-error', { error: error.message }, true);
}
}
function logEvent(type, data, isError = false) {
const eventsDiv = document.getElementById('events');
const eventDiv = document.createElement('div');
eventDiv.className = `event ${isError ? 'error' : type}`;
eventDiv.textContent = `[${type}] ${JSON.stringify(data, null, 2)}`;
eventsDiv.appendChild(eventDiv);
eventsDiv.scrollTop = eventsDiv.scrollHeight;
}
function clearEvents() {
document.getElementById('events').innerHTML = '';
}
</script>
</body>
</html>
Access it at: http://localhost:3010/test-sse.html
Step 5: Monitor Metrics
# Check EventBus and SSE metrics
curl http://localhost:3010/api/debug/metrics
# Expected response:
{
"eventBus": {
"eventsEmitted": 1234,
"eventsByType": { "session-output": 800, "session-error": 5, ... },
"listenerCounts": { "session-output-session-123": 1, ... },
"activeListeners": 10
},
"sse": {
"totalSessions": 3,
"totalConnections": 5,
"sessions": { "session-123": 2, "session-456": 1, ... },
"totalCreated": 50,
"totalClosed": 45,
"activeHeartbeats": 5
},
"timestamp": 1234567890
}
Step 6: Test Reconnection
- Start SSE connection in browser
- Kill and restart server
- Verify automatic reconnection with exponential backoff
- Check browser console for reconnection logs
Step 7: Load Testing
// In browser console, run:
const connections = [];
for (let i = 0; i < 10; i++) {
const sessionId = 'session-123'; // Use a real session ID
const stream = new SessionEventStream(sessionId);
connections.push(stream);
}
// Check metrics
fetch('/api/debug/metrics').then(r => r.json()).then(console.log);
// Cleanup
connections.forEach(s => s.disconnect());
Step 8: nginx Configuration
If using nginx as reverse proxy, add this configuration:
# In your nginx server block
location /api/session/ {
# Disable buffering for SSE
proxy_buffering off;
proxy_cache off;
# Pass to backend
proxy_pass http://localhost:3010;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_set_header Cache-Control no-cache;
proxy_set_header X-Accel-Buffering no;
# Increase timeouts for long-lived connections
proxy_read_timeout 86400s; # 24 hours
proxy_send_timeout 86400s;
# Ensure no buffering
proxy_buffering off;
}
Testing Checklist
- SSE connection established successfully
- Events received in real-time
- Multiple clients can connect to same session
- Reconnection works on connection drop
- Heartbeat prevents timeout
- Metrics endpoint returns correct data
- Session validation works (404 for invalid session)
- Command sent via REST API produces output via SSE
- No memory leaks after extended use
- Works through nginx reverse proxy
Troubleshooting
Issue: SSE connection closes immediately
Solution: Check nginx configuration, ensure X-Accel-Buffering: no is set.
Issue: No events received
Solution: Check session ID is valid and session exists. Check browser console for errors.
Issue: Frequent reconnections
Solution: Check network stability, increase heartbeatTimeout in client options.
Issue: Memory usage increasing
Solution: Check EventBus listeners are properly unsubscribed on disconnect.
Next Steps
After successful testing:
- Update existing UI to use SSE instead of WebSocket
- Add feature flag for gradual rollout
- Monitor production metrics
- Deprecate WebSocket endpoint
- Remove legacy code after migration complete