Files
moltbot/src/infra/heartbeat-events.ts
2026-03-22 18:09:45 +00:00

69 lines
2.0 KiB
TypeScript

import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import { notifyListeners, registerListener } from "../shared/listeners.js";
export type HeartbeatIndicatorType = "ok" | "alert" | "error";
export type HeartbeatEventPayload = {
ts: number;
status: "sent" | "ok-empty" | "ok-token" | "skipped" | "failed";
to?: string;
accountId?: string;
preview?: string;
durationMs?: number;
hasMedia?: boolean;
reason?: string;
/** The channel this heartbeat was sent to. */
channel?: string;
/** Whether the message was silently suppressed (showOk: false). */
silent?: boolean;
/** Indicator type for UI status display. */
indicatorType?: HeartbeatIndicatorType;
};
export function resolveIndicatorType(
status: HeartbeatEventPayload["status"],
): HeartbeatIndicatorType | undefined {
switch (status) {
case "ok-empty":
case "ok-token":
return "ok";
case "sent":
return "alert";
case "failed":
return "error";
case "skipped":
return undefined;
}
}
type HeartbeatEventState = {
lastHeartbeat: HeartbeatEventPayload | null;
listeners: Set<(evt: HeartbeatEventPayload) => void>;
};
const HEARTBEAT_EVENT_STATE_KEY = Symbol.for("openclaw.heartbeatEvents.state");
const state = resolveGlobalSingleton<HeartbeatEventState>(HEARTBEAT_EVENT_STATE_KEY, () => ({
lastHeartbeat: null,
listeners: new Set<(evt: HeartbeatEventPayload) => void>(),
}));
export function emitHeartbeatEvent(evt: Omit<HeartbeatEventPayload, "ts">) {
const enriched: HeartbeatEventPayload = { ts: Date.now(), ...evt };
state.lastHeartbeat = enriched;
notifyListeners(state.listeners, enriched);
}
export function onHeartbeatEvent(listener: (evt: HeartbeatEventPayload) => void): () => void {
return registerListener(state.listeners, listener);
}
export function getLastHeartbeatEvent(): HeartbeatEventPayload | null {
return state.lastHeartbeat;
}
export function resetHeartbeatEventsForTest(): void {
state.lastHeartbeat = null;
state.listeners.clear();
}