Skip to content

Commit f7fd317

Browse files
Refactor SSE event handling and consolidate notification contexts
1 parent 9851ec0 commit f7fd317

30 files changed

Lines changed: 2204 additions & 966 deletions

backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@opencode-manager/shared": "workspace:*",
2020
"archiver": "^7.0.1",
2121
"dotenv": "^17.2.3",
22+
"eventsource": "^4.1.0",
2223
"hono": "^4.10.1",
2324
"strip-json-comments": "^3.1.1",
2425
"zod": "^4.1.12"
@@ -28,6 +29,7 @@
2829
"@types/archiver": "^7.0.0",
2930
"@types/better-sqlite3": "^7.6.13",
3031
"@types/bun": "latest",
32+
"@types/eventsource": "^3.0.0",
3133
"@vitest/ui": "^3.2.4",
3234
"eslint": "^9.39.1",
3335
"typescript-eslint": "^8.45.0",

backend/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { createFileRoutes } from './routes/files'
1313
import { createProvidersRoutes } from './routes/providers'
1414
import { createOAuthRoutes } from './routes/oauth'
1515
import { createTitleRoutes } from './routes/title'
16+
import { createSSERoutes } from './routes/sse'
17+
import { sseAggregator } from './services/sse-aggregator'
1618
import { ensureDirectoryExists, writeFileContent, fileExists, readFileContent } from './services/file-operations'
1719
import { SettingsService } from './services/settings'
1820
import { opencodeServerManager } from './services/opencode-single-server'
@@ -230,6 +232,7 @@ app.route('/api/providers', createProvidersRoutes())
230232
app.route('/api/oauth', createOAuthRoutes())
231233
app.route('/api/tts', createTTSRoutes(db))
232234
app.route('/api/generate-title', createTitleRoutes())
235+
app.route('/api/sse', createSSERoutes())
233236

234237
app.all('/api/opencode/*', async (c) => {
235238
const request = c.req.raw
@@ -302,10 +305,12 @@ const shutdown = async (signal: string) => {
302305

303306
logger.info(`${signal} received, shutting down gracefully...`)
304307
try {
308+
sseAggregator.shutdown()
309+
logger.info('SSE Aggregator stopped')
305310
await opencodeServerManager.stop()
306311
logger.info('OpenCode server stopped')
307312
} catch (error) {
308-
logger.error('Error stopping OpenCode server:', error)
313+
logger.error('Error during shutdown:', error)
309314
}
310315
process.exit(0)
311316
}

backend/src/routes/sse.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Hono } from 'hono'
2+
import { streamSSE } from 'hono/streaming'
3+
import { sseAggregator } from '../services/sse-aggregator'
4+
import { SSESubscribeSchema } from '@opencode-manager/shared/schemas'
5+
import { logger } from '../utils/logger'
6+
7+
export function createSSERoutes() {
8+
const app = new Hono()
9+
10+
app.get('/stream', async (c) => {
11+
const directoriesParam = c.req.query('directories')
12+
const directories = directoriesParam ? directoriesParam.split(',').filter(Boolean) : []
13+
14+
return streamSSE(c, async (stream) => {
15+
const clientId = `client_${Date.now()}_${Math.random().toString(36).slice(2)}`
16+
17+
const cleanup = sseAggregator.addClient(
18+
clientId,
19+
(event, data) => {
20+
stream.writeSSE({ event, data })
21+
},
22+
directories
23+
)
24+
25+
stream.onAbort(() => {
26+
cleanup()
27+
})
28+
29+
try {
30+
await stream.writeSSE({
31+
event: 'connected',
32+
data: JSON.stringify({ clientId, directories, ...sseAggregator.getConnectionStatus() })
33+
})
34+
} catch (err) {
35+
logger.error(`Failed to send SSE connected event for ${clientId}:`, err)
36+
}
37+
38+
await new Promise(() => {})
39+
})
40+
})
41+
42+
app.post('/subscribe', async (c) => {
43+
const body = await c.req.json()
44+
const result = SSESubscribeSchema.safeParse(body)
45+
if (!result.success) {
46+
return c.json({ success: false, error: 'Invalid request', details: result.error.issues }, 400)
47+
}
48+
const success = sseAggregator.addDirectories(result.data.clientId, result.data.directories)
49+
if (!success) {
50+
return c.json({ success: false, error: 'Client not found' }, 404)
51+
}
52+
return c.json({ success: true })
53+
})
54+
55+
app.post('/unsubscribe', async (c) => {
56+
const body = await c.req.json()
57+
const result = SSESubscribeSchema.safeParse(body)
58+
if (!result.success) {
59+
return c.json({ success: false, error: 'Invalid request', details: result.error.issues }, 400)
60+
}
61+
const success = sseAggregator.removeDirectories(result.data.clientId, result.data.directories)
62+
if (!success) {
63+
return c.json({ success: false, error: 'Client not found' }, 404)
64+
}
65+
return c.json({ success: true })
66+
})
67+
68+
app.get('/status', (c) => {
69+
return c.json({
70+
...sseAggregator.getConnectionStatus(),
71+
clients: sseAggregator.getClientCount(),
72+
directories: sseAggregator.getActiveDirectories(),
73+
activeSessions: sseAggregator.getActiveSessions()
74+
})
75+
})
76+
77+
return app
78+
}

0 commit comments

Comments
 (0)