Skip to content

Commit 23f4bc1

Browse files
committed
feat(api): wire historian replay routes with real replayer, pass mqtt client to historian
1 parent 458c1e6 commit 23f4bc1

2 files changed

Lines changed: 63 additions & 23 deletions

File tree

packages/api/src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function createApp(options?: AppOptions | LibraryStorage): express.Expres
5757
// Co-simulation routes (with MQTT client injection)
5858
app.use("/api/v1/cosim", cosimRouter(mqttClient));
5959
app.use("/api/v1/mqtt/participants", mqttParticipantsRouter(mqttClient));
60-
app.use("/api/v1/historian", historianRouter(dbPool));
60+
app.use("/api/v1/historian", historianRouter(dbPool, mqttClient));
6161
app.use("/api/v1/fmus", fmuRouter());
6262

6363
// Health check

packages/api/src/routes/historian.ts

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
1-
// SPDX-License-Identifier: AGPL-3.0-or-later
2-
3-
/**
4-
* Historian REST routes.
5-
*
6-
* Provides endpoints for querying historical co-simulation data stored
7-
* in TimescaleDB, including time-series telemetry, session metadata,
8-
* and replay control.
9-
*/
10-
1+
import type { CosimMqttClient } from "@modelscript/cosim";
2+
import { HistorianReplayer } from "@modelscript/cosim";
113
import express from "express";
124
import type { Pool } from "pg";
135

@@ -23,11 +15,15 @@ interface HistorianQueryParams {
2315
interval?: string;
2416
}
2517

18+
/** Active replayer tracking. */
19+
const activeReplayers = new Map<string, HistorianReplayer>();
20+
2621
/**
2722
* Create the historian router.
2823
* @param pool PostgreSQL/TimescaleDB connection pool (null = stubs)
24+
* @param mqttClient MQTT client for replay publishing (null = stub responses)
2925
*/
30-
export function historianRouter(pool: Pool | null): express.Router {
26+
export function historianRouter(pool: Pool | null, mqttClient?: CosimMqttClient | null): express.Router {
3127
const router = express.Router();
3228

3329
// GET /api/v1/historian/sessions — List recorded sessions
@@ -174,8 +170,9 @@ export function historianRouter(pool: Pool | null): express.Router {
174170
variable_name: string;
175171
value: number;
176172
}[]) {
177-
if (!values[row.participant_id]) values[row.participant_id] = {};
178-
values[row.participant_id][row.variable_name] = row.value;
173+
const pid = row.participant_id;
174+
values[pid] = values[pid] ?? {};
175+
values[pid][row.variable_name] = row.value;
179176
}
180177

181178
res.json({ sessionId: query.sessionId, values });
@@ -256,25 +253,68 @@ export function historianRouter(pool: Pool | null): express.Router {
256253
return res.status(400).json({ error: "Missing required field: sessionId" });
257254
}
258255

256+
if (!pool || !mqttClient) {
257+
return res.status(503).json({ error: "Historian replay requires TimescaleDB and MQTT" });
258+
}
259+
259260
const replayId = `replay-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
260-
res.json({ ok: true, replayId, sessionId, from: from ?? null, to: to ?? null, speedFactor, state: "playing" });
261+
const replayer = new HistorianReplayer(pool, mqttClient);
262+
activeReplayers.set(replayId, replayer);
263+
264+
// Start replay asynchronously
265+
const replayFrom = from ? new Date(from) : new Date(0);
266+
const replayTo = to ? new Date(to) : new Date();
267+
268+
void replayer
269+
.replay({
270+
sessionId,
271+
from: replayFrom,
272+
to: replayTo,
273+
speedFactor,
274+
})
275+
.then(() => {
276+
// Auto-cleanup on completion
277+
if (replayer.state === "completed" || replayer.state === "error") {
278+
activeReplayers.delete(replayId);
279+
}
280+
});
281+
282+
res.json({ ok: true, replayId, sessionId, speedFactor, state: replayer.state });
261283
});
262284

263-
// POST /api/v1/historian/replay/pause — Pause replay
264-
router.post("/replay/pause", (_req, res) => {
265-
res.json({ ok: true, state: "paused" });
285+
// POST /api/v1/historian/replay/:id/pause — Pause replay
286+
router.post("/replay/:id/pause", (req, res) => {
287+
const replayer = activeReplayers.get(req.params["id"] ?? "");
288+
if (!replayer) return res.status(404).json({ error: "Replay not found" });
289+
replayer.pause();
290+
res.json({ ok: true, state: replayer.state });
266291
});
267292

268-
// POST /api/v1/historian/replay/resume — Resume replay
269-
router.post("/replay/resume", (_req, res) => {
270-
res.json({ ok: true, state: "playing" });
293+
// POST /api/v1/historian/replay/:id/resume — Resume replay
294+
router.post("/replay/:id/resume", (req, res) => {
295+
const replayer = activeReplayers.get(req.params["id"] ?? "");
296+
if (!replayer) return res.status(404).json({ error: "Replay not found" });
297+
replayer.resume();
298+
res.json({ ok: true, state: replayer.state });
271299
});
272300

273-
// POST /api/v1/historian/replay/stop — Stop replay
274-
router.post("/replay/stop", (_req, res) => {
301+
// POST /api/v1/historian/replay/:id/stop — Stop replay
302+
router.post("/replay/:id/stop", (req, res) => {
303+
const replayId = req.params["id"] ?? "";
304+
const replayer = activeReplayers.get(replayId);
305+
if (!replayer) return res.status(404).json({ error: "Replay not found" });
306+
replayer.stop();
307+
activeReplayers.delete(replayId);
275308
res.json({ ok: true, state: "idle" });
276309
});
277310

311+
// GET /api/v1/historian/replay/:id — Get replay status
312+
router.get("/replay/:id", (req, res) => {
313+
const replayer = activeReplayers.get(req.params["id"] ?? "");
314+
if (!replayer) return res.status(404).json({ error: "Replay not found" });
315+
res.json({ state: replayer.state, error: replayer.error });
316+
});
317+
278318
// DELETE /api/v1/historian/sessions/:id — Delete recorded session data
279319
router.delete("/sessions/:id", async (req, res) => {
280320
const sessionId = req.params["id"] ?? "";

0 commit comments

Comments
 (0)