feat(api): wire mqtt client, timescaledb pool, historian recorder, pa… · modelscript/modelscript@1daa6e0 · GitHub
Skip to content

Commit 1daa6e0

Browse files
committed
feat(api): wire mqtt client, timescaledb pool, historian recorder, participant enrollment
1 parent 9d30f91 commit 1daa6e0

4 files changed

Lines changed: 398 additions & 110 deletions

File tree

‎packages/api/src/app.ts‎

Lines changed: 30 additions & 7 deletions

‎packages/api/src/main.ts‎

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,37 @@
66
* Wires together:
77
* - Express app with all route modules
88
* - MQTT client for co-simulation participant discovery
9+
* - TimescaleDB pool for historian queries
10+
* - Historian recorder for live telemetry ingestion
911
* - WebSocket server for live variable streaming
1012
* - Graceful shutdown handling
1113
*/
1214

13-
import { CosimMqttClient, attachCosimWebSocket } from "@modelscript/cosim";
15+
import { CosimMqttClient, HistorianRecorder, attachCosimWebSocket } from "@modelscript/cosim";
1416
import { createApp } from "./app.js";
1517

1618
const port = parseInt(process.env["PORT"] ?? "3000", 10);
1719
const mqttUrl = process.env["MQTT_BROKER_URL"];
18-
19-
const app = createApp();
20-
21-
// ── Start HTTP server ──
22-
23-
const server = app.listen(port, () => {
24-
console.log(`ModelScript API server listening on port ${port}`);
25-
});
20+
const timescaleUrl = process.env["TIMESCALE_URL"];
21+
22+
// ── TimescaleDB pool (optional — only if TIMESCALE_URL is set) ──
23+
24+
let dbPool: import("pg").Pool | null = null;
25+
26+
if (timescaleUrl) {
27+
// Lazy-load pg to avoid hard dependency when TimescaleDB is unavailable
28+
try {
29+
const { Pool } = await import("pg");
30+
dbPool = new Pool({ connectionString: timescaleUrl, max: 10 });
31+
// Test the connection
32+
await dbPool.query("SELECT 1");
33+
console.log("TimescaleDB connected.");
34+
} catch (err: unknown) {
35+
console.error("TimescaleDB connection failed:", err instanceof Error ? err.message : err);
36+
console.warn("Historian features will be unavailable.");
37+
dbPool = null;
38+
}
39+
}
2640

2741
// ── MQTT client (optional — only if MQTT_BROKER_URL is set) ──
2842

@@ -38,23 +52,41 @@ if (mqttUrl) {
3852
},
3953
});
4054

41-
mqttClient
42-
.connect()
43-
.then(async () => {
44-
console.log(`MQTT connected to ${mqttUrl}`);
45-
if (mqttClient) {
46-
await mqttClient.subscribeParticipants();
47-
console.log(`MQTT participant discovery active (${mqttClient.participants.size} online)`);
48-
}
49-
})
50-
.catch((err: unknown) => {
51-
console.error("MQTT connection failed:", err instanceof Error ? err.message : err);
52-
console.warn("Co-simulation features will be unavailable.");
53-
mqttClient = null;
54-
});
55+
try {
56+
await mqttClient.connect();
57+
console.log(`MQTT connected to ${mqttUrl}`);
58+
await mqttClient.subscribeParticipants();
59+
console.log(`MQTT participant discovery active (${mqttClient.participants.size} online)`);
60+
} catch (err: unknown) {
61+
console.error("MQTT connection failed:", err instanceof Error ? err.message : err);
62+
console.warn("Co-simulation features will be unavailable.");
63+
mqttClient = null;
64+
}
5565
}
5666

57-
// ── WebSocket streaming (attaches to the HTTP server upgrade event) ──
67+
// ── Historian recorder (only when both MQTT and TimescaleDB are available) ──
68+
69+
let recorder: HistorianRecorder | null = null;
70+
71+
if (mqttClient && dbPool) {
72+
recorder = new HistorianRecorder(dbPool, mqttClient, {
73+
batchSize: 500,
74+
flushIntervalMs: 200,
75+
});
76+
console.log("Historian recorder ready (starts per-session).");
77+
}
78+
79+
// ── Create Express app ──
80+
81+
const app = createApp({ mqttClient, dbPool });
82+
83+
// ── Start HTTP server ──
84+
85+
const server = app.listen(port, () => {
86+
console.log(`ModelScript API server listening on port ${port}`);
87+
});
88+
89+
// ── WebSocket streaming ──
5890

5991
attachCosimWebSocket(server, mqttClient);
6092
console.log("WebSocket co-simulation stream available at /api/v1/cosim/stream");
@@ -64,10 +96,16 @@ console.log("WebSocket co-simulation stream available at /api/v1/cosim/stream");
6496
async function shutdown(signal: string): Promise<void> {
6597
console.log(`\n${signal} received. Shutting down gracefully...`);
6698

67-
// Stop accepting new connections
6899
server.close();
69100

70-
// Disconnect MQTT
101+
if (recorder) {
102+
try {
103+
await recorder.stopRecording();
104+
} catch {
105+
// Best-effort
106+
}
107+
}
108+
71109
if (mqttClient) {
72110
try {
73111
await mqttClient.disconnect();
@@ -77,6 +115,15 @@ async function shutdown(signal: string): Promise<void> {
77115
}
78116
}
79117

118+
if (dbPool) {
119+
try {
120+
await dbPool.end();
121+
console.log("TimescaleDB pool closed.");
122+
} catch {
123+
// Best-effort
124+
}
125+
}
126+
80127
process.exit(0);
81128
}
82129

‎packages/api/src/routes/cosim.ts‎

Lines changed: 107 additions & 13 deletions

0 commit comments

Comments
 (0)