mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: route internal events through a shared plugin event bus
Formalises three ad-hoc publish paths that already existed: automation-events.ts, metrics/automation-bridge.ts and the hostSessionStatus singleton, now the host.session.status topic. Both invariants the old code relied on are preserved. Publishing stays fire-and-forget, so a failing subscriber cannot disturb a metrics poll or a host delete. The automations engine subscribes to the bus at scheduler start instead of being imported ad hoc, which keeps the edge one-directional: automations may import repositories, hosts modules must not import automations.
This commit is contained in:
1 parent
0a771c9600
commit
1393fa7581
9 files changed
+433
-52
No files matched your search
@@ -1,17 +1,24 @@
|
||||
/**
|
||||
* One-line hand-off from any hosts feature to the automations engine for a
|
||||
* generic named event (not tied to metrics polling).
|
||||
* One-line hand-off from any hosts feature to anything listening for a generic
|
||||
* named event (not tied to metrics polling).
|
||||
*
|
||||
* Fire-and-forget and imported lazily: a failure in the automations layer
|
||||
* must never disturb the caller, and a static import would create a cycle
|
||||
* (automations reads repositories, which several hosts modules also pull in).
|
||||
*
|
||||
* PLUGIN-EVENT: this whole module is the "publish an internal event" half of
|
||||
* the phase-2 ctx.events bus. Once that exists, callers should emit onto
|
||||
* ctx.events instead of calling this directly, and the automations engine
|
||||
* subscribes there rather than being imported ad hoc.
|
||||
* This now publishes onto the ctx.events bus rather than reaching into the
|
||||
* automations engine directly. The two properties callers relied on are
|
||||
* unchanged: it is fire-and-forget, and it creates no static edge to the
|
||||
* automations layer (which reads repositories that several hosts modules also
|
||||
* pull in, so a static import would close a cycle). The automations engine
|
||||
* subscribes to the bus at start-up instead -- see automations/triggers.ts.
|
||||
*/
|
||||
|
||||
import { pluginEvents, TOPICS } from "../plugins/events.js";
|
||||
|
||||
export interface InternalEventPayload {
|
||||
event: string;
|
||||
userId: string;
|
||||
hostId?: number;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function notifyAutomationInternalEvent(
|
||||
event: string,
|
||||
userId: string,
|
||||
@@ -19,9 +26,10 @@ export function notifyAutomationInternalEvent(
|
||||
details?: Record<string, unknown>,
|
||||
): void {
|
||||
if (!userId) return;
|
||||
import("../automations/triggers.js")
|
||||
.then((triggers) =>
|
||||
triggers.onInternalEvent({ event, userId, hostId, details }),
|
||||
)
|
||||
.catch(() => {});
|
||||
pluginEvents.emit(TOPICS.internalEvent, {
|
||||
event,
|
||||
userId,
|
||||
hostId,
|
||||
details,
|
||||
} satisfies InternalEventPayload);
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import { pluginEvents, TOPICS } from "../plugins/events.js";
|
||||
|
||||
type HostSessionStatusListener = (hostId: number, online: boolean) => void;
|
||||
|
||||
export interface HostSessionStatusPayload {
|
||||
hostId: number;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export class HostSessionStatus {
|
||||
private counts = new Map<number, number>();
|
||||
private listeners = new Set<HostSessionStatusListener>();
|
||||
@@ -32,6 +39,12 @@ export class HostSessionStatus {
|
||||
|
||||
private emit(hostId: number, online: boolean): void {
|
||||
for (const listener of this.listeners) listener(hostId, online);
|
||||
// Also published as a topic so plugins (and anything else on the bus) can
|
||||
// observe session status without holding a reference to this singleton.
|
||||
pluginEvents.emit(TOPICS.hostSessionStatus, {
|
||||
hostId,
|
||||
online,
|
||||
} satisfies HostSessionStatusPayload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
/**
|
||||
* One-line hand-off from the metrics poller to the automations engine.
|
||||
* One-line hand-off from the metrics poller onto the ctx.events bus.
|
||||
*
|
||||
* The poller is already a very large module, so the hooks it calls live here
|
||||
* instead. Everything is fire-and-forget and imported lazily: a failure in the
|
||||
* automations layer must never disturb metric collection, and a static import
|
||||
* would create a cycle (automations reads repositories, which the metrics
|
||||
* module also pulls in).
|
||||
* instead. Everything is fire-and-forget: a failure in a subscriber must never
|
||||
* disturb metric collection, and publishing onto the bus creates no static edge
|
||||
* to the automations layer (which reads repositories the metrics module also
|
||||
* pulls in).
|
||||
*
|
||||
* For the generic (non-metrics) internal event notifier, see
|
||||
* `hosts/automation-events.ts` -- that one is shared across features.
|
||||
*/
|
||||
|
||||
import { pluginEvents, TOPICS } from "../../plugins/events.js";
|
||||
import type { MetricsSnapshot } from "../../automations/conditions.js";
|
||||
|
||||
export interface HostMetricsPayload {
|
||||
hostId: number;
|
||||
ownerUserId: string;
|
||||
metrics: MetricsSnapshot;
|
||||
}
|
||||
|
||||
export interface HostStatusPayload {
|
||||
hostId: number;
|
||||
ownerUserId: string;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
export interface HostHealthCheckPayload {
|
||||
hostId: number;
|
||||
userId: string;
|
||||
checkId: string;
|
||||
ok: boolean;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export function notifyAutomationMetrics(
|
||||
hostId: number,
|
||||
ownerUserId: string,
|
||||
metrics: MetricsSnapshot,
|
||||
): void {
|
||||
if (!ownerUserId) return;
|
||||
import("../../automations/triggers.js")
|
||||
.then((triggers) => triggers.onMetrics({ hostId, ownerUserId, metrics }))
|
||||
.catch(() => {});
|
||||
pluginEvents.emit(TOPICS.hostMetrics, {
|
||||
hostId,
|
||||
ownerUserId,
|
||||
metrics,
|
||||
} satisfies HostMetricsPayload);
|
||||
}
|
||||
|
||||
export function notifyAutomationStatus(
|
||||
@@ -30,9 +53,11 @@ export function notifyAutomationStatus(
|
||||
online: boolean,
|
||||
): void {
|
||||
if (!ownerUserId) return;
|
||||
import("../../automations/triggers.js")
|
||||
.then((triggers) => triggers.onStatus({ hostId, ownerUserId, online }))
|
||||
.catch(() => {});
|
||||
pluginEvents.emit(TOPICS.hostStatus, {
|
||||
hostId,
|
||||
ownerUserId,
|
||||
online,
|
||||
} satisfies HostStatusPayload);
|
||||
}
|
||||
|
||||
export function notifyAutomationHealthCheck(
|
||||
@@ -43,9 +68,11 @@ export function notifyAutomationHealthCheck(
|
||||
detail?: string,
|
||||
): void {
|
||||
if (!userId) return;
|
||||
import("../../automations/triggers.js")
|
||||
.then((triggers) =>
|
||||
triggers.onHealthCheck({ hostId, userId, checkId, ok, detail }),
|
||||
)
|
||||
.catch(() => {});
|
||||
pluginEvents.emit(TOPICS.hostHealthCheck, {
|
||||
hostId,
|
||||
userId,
|
||||
checkId,
|
||||
ok,
|
||||
detail,
|
||||
} satisfies HostHealthCheckPayload);
|
||||
}
|
||||
@@ -61,8 +61,8 @@ import { registerHostMetricsHistoryRoutes } from "./history-routes.js";
|
||||
import { registerProxmoxStatsRoutes } from "./proxmox-stats-routes.js";
|
||||
import { registerProxmoxStatsHistoryRoutes } from "./proxmox-stats-history-routes.js";
|
||||
import { ProxmoxPollingManager } from "./proxmox-stats-polling.js";
|
||||
// PLUGIN-EVENT: terminal session online/offline -> phase-2 ctx.events "host.session.status" topic
|
||||
import { hostSessionStatus } from "../host-session-status.js";
|
||||
import type { HostSessionStatusPayload } from "../host-session-status.js";
|
||||
import { pluginEvents, TOPICS } from "../../plugins/events.js";
|
||||
import { AlertEngine } from "./alert-engine.js";
|
||||
import {
|
||||
notifyAutomationMetrics,
|
||||
@@ -225,10 +225,12 @@ class PollingManager {
|
||||
private unsubscribeHostSessionStatus: () => void;
|
||||
|
||||
constructor() {
|
||||
// PLUGIN-EVENT: subscribe to ctx.events.on("host.session.status", ...) once the
|
||||
// phase-2 event bus exists, instead of the hostSessionStatus singleton directly.
|
||||
this.unsubscribeHostSessionStatus = hostSessionStatus.subscribe(
|
||||
(hostId, online) => this.setTerminalSessionOnline(hostId, online),
|
||||
this.unsubscribeHostSessionStatus = pluginEvents.on(
|
||||
TOPICS.hostSessionStatus,
|
||||
(payload) => {
|
||||
const { hostId, online } = payload as HostSessionStatusPayload;
|
||||
this.setTerminalSessionOnline(hostId, online);
|
||||
},
|
||||
);
|
||||
this.viewerCleanupInterval = setInterval(() => {
|
||||
this.cleanupInactiveViewers();
|
||||
|
||||
Reference in new issue
Block a user