Files
zn-ai/electron/gateway/connection-monitor.ts
DEV_DSW 71bcc3b3c5 feat: implement telemetry system for application usage tracking
- Added telemetry utility to capture application events and metrics.
- Integrated PostHog for event tracking with distinct user identification.
- Implemented telemetry initialization, event capturing, and shutdown procedures.

feat: add UV environment setup for Python management

- Created utilities to manage Python installation and configuration.
- Implemented network optimization checks for Python installation mirrors.
- Added functions to set up managed Python environments with error handling.

feat: enhance host API communication with token management

- Introduced host API token retrieval and management for secure requests.
- Updated host API fetch functions to include token in headers.
- Added support for creating event sources with authentication.

test: add comprehensive tests for gateway protocol and startup helpers

- Implemented unit tests for gateway protocol helpers, event dispatching, and state management.
- Added tests for startup recovery strategies and process policies.
- Ensured coverage for connection monitoring and restart governance logic.
2026-04-23 17:21:57 +08:00

123 lines
3.5 KiB
TypeScript

import logManager from '@electron/service/logger';
type HealthResult = { ok: boolean; error?: string };
type HeartbeatAliveReason = 'pong' | 'message';
type PingOptions = {
sendPing: () => void;
onHeartbeatTimeout: (context: { consecutiveMisses: number; timeoutMs: number }) => void;
intervalMs?: number;
timeoutMs?: number;
maxConsecutiveMisses?: number;
};
export class GatewayConnectionMonitor {
private pingInterval: NodeJS.Timeout | null = null;
private healthCheckInterval: NodeJS.Timeout | null = null;
private lastPingAt = 0;
private waitingForAlive = false;
private consecutiveMisses = 0;
private timeoutTriggered = false;
startPing(options: PingOptions): void {
const intervalMs = options.intervalMs ?? 30000;
const timeoutMs = options.timeoutMs ?? 10000;
const maxConsecutiveMisses = Math.max(1, options.maxConsecutiveMisses ?? 3);
this.resetHeartbeatState();
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
this.pingInterval = setInterval(() => {
const now = Date.now();
if (this.waitingForAlive && now - this.lastPingAt >= timeoutMs) {
this.waitingForAlive = false;
this.consecutiveMisses += 1;
logManager.warn(
`Gateway heartbeat missed (${this.consecutiveMisses}/${maxConsecutiveMisses}, timeout=${timeoutMs}ms)`,
);
if (this.consecutiveMisses >= maxConsecutiveMisses && !this.timeoutTriggered) {
this.timeoutTriggered = true;
options.onHeartbeatTimeout({
consecutiveMisses: this.consecutiveMisses,
timeoutMs,
});
return;
}
}
options.sendPing();
this.waitingForAlive = true;
this.lastPingAt = now;
}, intervalMs);
}
markAlive(reason: HeartbeatAliveReason): void {
if (this.consecutiveMisses > 0) {
logManager.debug(`Gateway heartbeat recovered via ${reason} (misses=${this.consecutiveMisses})`);
}
this.waitingForAlive = false;
this.consecutiveMisses = 0;
this.timeoutTriggered = false;
}
handlePong(): void {
this.markAlive('pong');
}
getConsecutiveMisses(): number {
return this.consecutiveMisses;
}
startHealthCheck(options: {
shouldCheck: () => boolean;
checkHealth: () => Promise<HealthResult>;
onUnhealthy: (errorMessage: string) => void;
onError: (error: unknown) => void;
intervalMs?: number;
}): void {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
}
this.healthCheckInterval = setInterval(async () => {
if (!options.shouldCheck()) {
return;
}
try {
const health = await options.checkHealth();
if (!health.ok) {
const errorMessage = health.error ?? 'Health check failed';
logManager.warn(`Gateway health check failed: ${errorMessage}`);
options.onUnhealthy(errorMessage);
}
} catch (error) {
logManager.error('Gateway health check error:', error);
options.onError(error);
}
}, options.intervalMs ?? 30000);
}
clear(): void {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
this.resetHeartbeatState();
}
private resetHeartbeatState(): void {
this.lastPingAt = 0;
this.waitingForAlive = false;
this.consecutiveMisses = 0;
this.timeoutTriggered = false;
}
}