feat(beszel): add SSE subscription endpoint and per-period cache TTL

- Add subscribeSystemStats tRPC subscription bridging PocketBase SSE
- Add subscribeRealtimeMetrics to BeszelIntegration with SSE parsing
- Add error handling: try/catch in subscription, response check in topic POST
- Add cacheDurationForInput to request handler for per-period cache TTL
- Add mock subscribeRealtimeMetrics with 1s interval for dev testing
This commit is contained in:
Thomas Camlong committed 2026-06-15 14:58:47 +02:00
1 parent 5035c26dbd
commit c9769bf530
6 files changed
+272 -49

No files matched your search

+86 -14
View File
@@ -1,12 +1,17 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod/v4";
import { createIntegrationAsync } from "@homarr/integrations";
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
import {
beszelAlertsRequestHandler,
beszelStatsRequestHandler,
beszelSystemsRequestHandler,
} from "@homarr/request-handler/beszel";
import type { BeszelAlertsData, BeszelSystemRow } from "@homarr/request-handler/beszel";
import type {
BeszelAlertsData,
BeszelSystemRow,
} from "@homarr/request-handler/beszel";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
@@ -17,8 +22,14 @@ export const beszelRouter = createTRPCRouter({
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = beszelSystemsRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
const innerHandler = beszelSystemsRequestHandler.handler(
integration,
{},
);
const { data, timestamp } =
await innerHandler.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
return {
integrationId: integration.id,
integrationName: integration.name,
@@ -34,9 +45,16 @@ export const beszelRouter = createTRPCRouter({
subscribeSystems: publicProcedure
.concat(createManyIntegrationMiddleware("query", "beszel", "mock"))
.subscription(({ ctx }) => {
return observable<{ integrationId: string; systems: BeszelSystemRow[]; timestamp: Date }>((emit) => {
return observable<{
integrationId: string;
systems: BeszelSystemRow[];
timestamp: Date;
}>((emit) => {
const unsubscribes = ctx.integrations.map((integration) => {
const innerHandler = beszelSystemsRequestHandler.handler(integration, {});
const innerHandler = beszelSystemsRequestHandler.handler(
integration,
{},
);
return innerHandler.subscribe((systems) => {
emit.next({
integrationId: integration.id,
@@ -62,11 +80,17 @@ export const beszelRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const alertsHandler = beszelAlertsRequestHandler.handler(integration, {
includeHistory: input.includeHistory,
maxHistoryItems: input.maxHistoryItems,
});
const systemsHandler = beszelSystemsRequestHandler.handler(integration, {});
const alertsHandler = beszelAlertsRequestHandler.handler(
integration,
{
includeHistory: input.includeHistory,
maxHistoryItems: input.maxHistoryItems,
},
);
const systemsHandler = beszelSystemsRequestHandler.handler(
integration,
{},
);
const [alertsResult, systemsResult] = await Promise.all([
alertsHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }),
systemsHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }),
@@ -96,14 +120,22 @@ export const beszelRouter = createTRPCRouter({
}),
)
.subscription(({ ctx, input }) => {
return observable<{ integrationId: string; alerts: BeszelAlertsData; timestamp: Date }>((emit) => {
return observable<{
integrationId: string;
alerts: BeszelAlertsData;
timestamp: Date;
}>((emit) => {
const unsubscribes = ctx.integrations.map((integration) => {
const innerHandler = beszelAlertsRequestHandler.handler(integration, {
includeHistory: input.includeHistory,
maxHistoryItems: input.maxHistoryItems,
});
return innerHandler.subscribe((data) => {
emit.next({ integrationId: integration.id, alerts: data, timestamp: new Date() });
emit.next({
integrationId: integration.id,
alerts: data,
timestamp: new Date(),
});
});
});
return () => {
@@ -117,7 +149,7 @@ export const beszelRouter = createTRPCRouter({
.input(
z.object({
systemId: z.string(),
timePeriod: z.enum(["1h", "12h", "24h", "1w", "30d"]),
timePeriod: z.enum(["1m", "1h", "12h", "24h", "1w", "30d"]),
includeDocker: z.boolean().default(true),
}),
)
@@ -129,7 +161,47 @@ export const beszelRouter = createTRPCRouter({
timePeriod: input.timePeriod,
includeDocker: input.includeDocker,
});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
const { data, timestamp } =
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return { integrationId: integration.id, ...data, updatedAt: timestamp };
}),
subscribeSystemStats: publicProcedure
.concat(createManyIntegrationMiddleware("query", "beszel", "mock"))
.input(
z.object({
systemId: z.string(),
includeDocker: z.boolean().default(true),
}),
)
.subscription(({ ctx, input }) => {
return observable<{
stats: BeszelSystemStatsRecord;
containerStats: BeszelContainerStatsRecord | null;
}>((emit) => {
const integration = ctx.integrations[0];
if (!integration) return () => {};
const abortController = new AbortController();
void (async () => {
try {
const instance = await createIntegrationAsync(integration);
await instance.subscribeRealtimeMetrics(
input.systemId,
(data) => emit.next(data),
abortController.signal,
);
} catch (error) {
if (!abortController.signal.aborted) {
emit.error(error instanceof Error ? error : new Error(String(error)));
}
}
})();
return () => {
abortController.abort();
};
});
}),
});