Skip to content

Commit 564f405

Browse files
committed
feat(dashboard): Wired dashboard server
1 parent 4e8c916 commit 564f405

3 files changed

Lines changed: 186 additions & 0 deletions

File tree

src/dashboard-service/app.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { createDashboardRouter } from './api/dashboard-router'
2+
import { createLogger } from '../factories/logger-factory'
3+
import { DashboardServiceConfig } from './config'
4+
import { DashboardWebSocketHub } from './ws/dashboard-ws-hub'
5+
import express from 'express'
6+
import { getHealthRequestHandler } from './handlers/request-handlers/get-health-request-handler'
7+
import http from 'http'
8+
import { PollingScheduler } from './polling/polling-scheduler'
9+
import { SnapshotService } from './services/snapshot-service'
10+
import { WebSocketServer } from 'ws'
11+
12+
const debug = createLogger('dashboard-service:app')
13+
14+
export interface DashboardService {
15+
readonly config: DashboardServiceConfig
16+
readonly snapshotService: SnapshotService
17+
readonly pollingScheduler: PollingScheduler
18+
start(): Promise<void>
19+
stop(): Promise<void>
20+
getHttpPort(): number
21+
}
22+
23+
export const createDashboardService = (config: DashboardServiceConfig): DashboardService => {
24+
console.info(
25+
'dashboard-service: creating service (host=%s, port=%d, wsPath=%s, pollIntervalMs=%d)',
26+
config.host,
27+
config.port,
28+
config.wsPath,
29+
config.pollIntervalMs,
30+
)
31+
32+
const snapshotService = new SnapshotService()
33+
34+
const app = express()
35+
.disable('x-powered-by')
36+
.get('/healthz', getHealthRequestHandler)
37+
.use('/api/v1/kpis', createDashboardRouter(snapshotService))
38+
39+
const webServer = http.createServer(app)
40+
const webSocketServer = new WebSocketServer({
41+
server: webServer,
42+
path: config.wsPath,
43+
})
44+
45+
const webSocketHub = new DashboardWebSocketHub(webSocketServer, () => snapshotService.getSnapshot())
46+
47+
const pollingScheduler = new PollingScheduler(config.pollIntervalMs, () => {
48+
const nextSnapshot = snapshotService.refreshPlaceholder()
49+
debug('poll tick produced snapshot sequence=%d', nextSnapshot.sequence)
50+
webSocketHub.broadcastTick(nextSnapshot.sequence)
51+
webSocketHub.broadcastSnapshot(nextSnapshot)
52+
})
53+
54+
const start = async () => {
55+
if (webServer.listening) {
56+
debug('start requested but service is already listening')
57+
return
58+
}
59+
60+
console.info('dashboard-service: starting http and websocket servers')
61+
62+
await new Promise<void>((resolve, reject) => {
63+
webServer.listen(config.port, config.host, () => {
64+
const address = webServer.address()
65+
debug('listening on %o', address)
66+
console.info('dashboard-service: listening on %o', address)
67+
resolve()
68+
})
69+
webServer.once('error', (error) => {
70+
console.error('dashboard-service: failed to start server', error)
71+
reject(error)
72+
})
73+
})
74+
75+
pollingScheduler.start()
76+
console.info('dashboard-service: polling scheduler started')
77+
}
78+
79+
const stop = async () => {
80+
console.info('dashboard-service: stopping service')
81+
pollingScheduler.stop()
82+
webSocketHub.close()
83+
await new Promise<void>((resolve, reject) => {
84+
if (!webServer.listening) {
85+
debug('stop requested while server was already stopped')
86+
resolve()
87+
return
88+
}
89+
90+
webServer.close((error) => {
91+
if (error) {
92+
console.error('dashboard-service: failed to stop cleanly', error)
93+
reject(error)
94+
return
95+
}
96+
97+
console.info('dashboard-service: http server closed')
98+
resolve()
99+
})
100+
})
101+
}
102+
103+
const getHttpPort = (): number => {
104+
const address = webServer.address()
105+
return typeof address === 'object' && address !== null ? address.port : config.port
106+
}
107+
108+
return {
109+
config,
110+
snapshotService,
111+
pollingScheduler,
112+
start,
113+
stop,
114+
getHttpPort,
115+
}
116+
}

src/dashboard-service/config.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export interface DashboardServiceConfig {
2+
host: string
3+
port: number
4+
wsPath: string
5+
pollIntervalMs: number
6+
}
7+
8+
const parseInteger = (value: string | undefined, fallback: number): number => {
9+
if (typeof value === 'undefined' || value === '') {
10+
return fallback
11+
}
12+
13+
const parsed = Number(value)
14+
if (!Number.isInteger(parsed) || parsed < 0) {
15+
return fallback
16+
}
17+
18+
return parsed
19+
}
20+
21+
export const getDashboardServiceConfig = (): DashboardServiceConfig => {
22+
return {
23+
host: process.env.DASHBOARD_SERVICE_HOST ?? '127.0.0.1',
24+
port: parseInteger(process.env.DASHBOARD_SERVICE_PORT, 8011),
25+
wsPath: process.env.DASHBOARD_WS_PATH ?? '/api/v1/kpis/stream',
26+
pollIntervalMs: parseInteger(process.env.DASHBOARD_POLL_INTERVAL_MS, 5000),
27+
}
28+
}

src/dashboard-service/index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createLogger } from '../factories/logger-factory'
2+
3+
import { createDashboardService } from './app'
4+
import { getDashboardServiceConfig } from './config'
5+
6+
const debug = createLogger('dashboard-service:index')
7+
8+
const run = async () => {
9+
const config = getDashboardServiceConfig()
10+
console.info('dashboard-service: bootstrapping with config %o', config)
11+
const service = createDashboardService(config)
12+
13+
const shutdown = async () => {
14+
console.info('dashboard-service: received shutdown signal')
15+
debug('received shutdown signal')
16+
await service.stop()
17+
process.exit(0)
18+
}
19+
20+
process
21+
.on('SIGINT', shutdown)
22+
.on('SIGTERM', shutdown)
23+
24+
process.on('uncaughtException', (error) => {
25+
console.error('dashboard-service: uncaught exception', error)
26+
})
27+
28+
process.on('unhandledRejection', (error) => {
29+
console.error('dashboard-service: unhandled rejection', error)
30+
})
31+
32+
await service.start()
33+
}
34+
35+
if (require.main === module) {
36+
run().catch((error) => {
37+
console.error('dashboard-service: unable to start', error)
38+
process.exit(1)
39+
})
40+
}
41+
42+
export { run }

0 commit comments

Comments
 (0)