navflow-browser-server
Version:
Standalone Playwright browser server for NavFlow - enables browser automation with API key authentication, workspace device management, session sync, LLM discovery tools, and requires Node.js v22+
210 lines • 7.86 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HeartbeatService = void 0;
const axios_1 = __importDefault(require("axios"));
const UpdateService_1 = require("./UpdateService");
class HeartbeatService {
constructor(browserManager, apiKey, version, proxyServerUrl) {
this.intervalId = null;
this.deviceId = null;
this.isShuttingDown = false;
this.browserManager = browserManager;
this.apiKey = apiKey;
this.version = version;
this.proxyServerUrl = proxyServerUrl || 'https://navflow-proxy-858493283701.us-central1.run.app';
this.startTime = new Date();
this.updateService = new UpdateService_1.UpdateService('navflow-browser-server', version, async () => {
console.log('💤 Preparing for auto-update shutdown...');
this.stop();
// Give time for offline heartbeat to send
await new Promise(resolve => setTimeout(resolve, 1000));
});
}
/**
* Start the heartbeat service
*/
start() {
if (this.intervalId) {
console.warn('Heartbeat service is already running');
return;
}
console.log('🔄 Starting heartbeat service...');
// Send initial heartbeat immediately
this.sendHeartbeat();
// Set up periodic heartbeat every 30 seconds
this.intervalId = setInterval(() => {
if (!this.isShuttingDown) {
this.sendHeartbeat();
}
}, 30000);
console.log('✅ Heartbeat service started (30s interval)');
}
/**
* Stop the heartbeat service
*/
stop() {
this.isShuttingDown = true;
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
console.log('🛑 Heartbeat service stopped');
}
// Send final offline heartbeat
this.sendOfflineHeartbeat();
}
/**
* Get system metrics for heartbeat
*/
getSystemMetrics() {
const memoryUsage = process.memoryUsage();
const uptime = Date.now() - this.startTime.getTime();
const activeSessions = this.browserManager.getActiveSessions().length;
return {
activeSessions,
memoryUsage: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)}MB`,
uptime: this.formatUptime(uptime)
};
}
/**
* Format uptime in human readable format
*/
formatUptime(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0)
return `${days}d ${hours % 24}h`;
if (hours > 0)
return `${hours}h ${minutes % 60}m`;
if (minutes > 0)
return `${minutes}m ${seconds % 60}s`;
return `${seconds}s`;
}
/**
* Send heartbeat to proxy server
*/
async sendHeartbeat() {
try {
const payload = {
deviceId: this.deviceId || undefined,
apiKey: this.apiKey,
status: 'online',
metrics: this.getSystemMetrics(),
version: this.version,
timestamp: new Date().toISOString()
};
const response = await axios_1.default.post(`${this.proxyServerUrl}/api/devices/heartbeat`, payload, {
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
if (response.data.success) {
// Store device ID if returned
if (response.data.message && !this.deviceId) {
// Extract device ID from response if available
// Heartbeat successful but don't log to reduce console noise
}
// Check for updates
if (response.data.updateAvailable && response.data.latestVersion) {
console.log(`🆙 Update available: ${this.version} → ${response.data.latestVersion}`);
await this.handleUpdateAvailable(response.data.latestVersion);
}
}
else {
console.warn('⚠️ Heartbeat failed:', response.data.message);
}
}
catch (error) {
if (axios_1.default.isAxiosError(error)) {
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
console.warn('🔗 Proxy server unreachable, will retry...');
}
else if (error.response?.status === 401) {
console.error('🔑 Invalid API key for heartbeat');
}
else {
console.warn('⚠️ Heartbeat failed:', error.message);
}
}
else {
console.warn('⚠️ Heartbeat error:', error.message);
}
}
}
/**
* Send offline heartbeat when shutting down
*/
async sendOfflineHeartbeat() {
try {
const payload = {
deviceId: this.deviceId || undefined,
apiKey: this.apiKey,
status: 'offline',
metrics: this.getSystemMetrics(),
version: this.version,
timestamp: new Date().toISOString()
};
await axios_1.default.post(`${this.proxyServerUrl}/api/devices/heartbeat`, payload, {
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
});
console.log('📤 Offline heartbeat sent');
}
catch (error) {
// Ignore errors during shutdown
console.warn('⚠️ Failed to send offline heartbeat:', error);
}
}
/**
* Handle update available notification
*/
async handleUpdateAvailable(latestVersion) {
// Check if any flows are currently executing
const activeSessions = this.browserManager.getActiveSessions();
let hasActiveFlows = false;
// Check each session to see if it's actively being used
for (const sessionId of activeSessions) {
const session = await this.browserManager.getSession(sessionId);
if (session) {
// Consider a session active if it was used in the last 5 minutes
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
if (session.lastUsed > fiveMinutesAgo) {
hasActiveFlows = true;
break;
}
}
}
if (hasActiveFlows) {
console.log('🔄 Update available but flows are running, will check again later');
return;
}
console.log('🚀 No active flows detected, applying update...');
// Perform the auto-update
const updateSuccess = await this.updateService.performUpdate();
if (!updateSuccess) {
console.warn('⚠️ Auto-update failed, will try again later');
}
}
/**
* Set device ID (called after device registration)
*/
setDeviceId(deviceId) {
this.deviceId = deviceId;
console.log(`📱 Device ID set for heartbeat: ${deviceId}`);
}
/**
* Check if heartbeat service is running
*/
isRunning() {
return this.intervalId !== null && !this.isShuttingDown;
}
}
exports.HeartbeatService = HeartbeatService;
//# sourceMappingURL=HeartbeatService.js.map