hangwin--mcp-chrome
281 行
7.5 KiB
TypeScript
281 行
7.5 KiB
TypeScript
/**
|
|
* @fileoverview Offscreen Keepalive
|
|
* @description Keeps the MV3 service worker alive using an Offscreen Document + Port heartbeat.
|
|
*
|
|
* Architecture:
|
|
* - Offscreen connects to Background (Service Worker) via a named Port.
|
|
* - Offscreen sends periodic `keepalive.ping` messages while keepalive is enabled.
|
|
* - Background replies with `keepalive.pong` to confirm the channel is alive.
|
|
*
|
|
* Contract:
|
|
* - After `stop`, keepalive must fully stop: no ping loop, no Port, and no reconnection attempts.
|
|
* - After `start`, keepalive must (re)connect if needed and resume the ping loop.
|
|
*/
|
|
|
|
import {
|
|
RR_V3_KEEPALIVE_PORT_NAME,
|
|
DEFAULT_KEEPALIVE_PING_INTERVAL_MS,
|
|
type KeepaliveMessage,
|
|
} from '@/common/rr-v3-keepalive-protocol';
|
|
|
|
// ==================== Runtime Control Protocol ====================
|
|
|
|
const KEEPALIVE_CONTROL_MESSAGE_TYPE = 'rr_v3_keepalive.control' as const;
|
|
|
|
type KeepaliveControlCommand = 'start' | 'stop';
|
|
|
|
interface KeepaliveControlMessage {
|
|
type: typeof KEEPALIVE_CONTROL_MESSAGE_TYPE;
|
|
command: KeepaliveControlCommand;
|
|
}
|
|
|
|
function isKeepaliveControlMessage(value: unknown): value is KeepaliveControlMessage {
|
|
if (!value || typeof value !== 'object') return false;
|
|
const v = value as Record<string, unknown>;
|
|
if (v.type !== KEEPALIVE_CONTROL_MESSAGE_TYPE) return false;
|
|
return v.command === 'start' || v.command === 'stop';
|
|
}
|
|
|
|
// ==================== State ====================
|
|
|
|
let initialized = false;
|
|
let keepalivePort: chrome.runtime.Port | null = null;
|
|
let pingTimer: ReturnType<typeof setInterval> | null = null;
|
|
/** Whether keepalive is desired (set by start/stop commands from Background) */
|
|
let keepaliveDesired = false;
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
// ==================== Type Guards ====================
|
|
|
|
/**
|
|
* Type guard for KeepaliveMessage.
|
|
*/
|
|
function isKeepaliveMessage(value: unknown): value is KeepaliveMessage {
|
|
if (!value || typeof value !== 'object') return false;
|
|
const v = value as Record<string, unknown>;
|
|
|
|
const type = v.type;
|
|
if (
|
|
type !== 'keepalive.ping' &&
|
|
type !== 'keepalive.pong' &&
|
|
type !== 'keepalive.start' &&
|
|
type !== 'keepalive.stop'
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return typeof v.timestamp === 'number' && Number.isFinite(v.timestamp);
|
|
}
|
|
|
|
// ==================== Port Management ====================
|
|
|
|
/**
|
|
* Schedule a reconnect attempt to maintain the Port connection.
|
|
* Only reconnect while keepalive is desired.
|
|
*/
|
|
function scheduleReconnect(delayMs = 1000): void {
|
|
if (!initialized) return;
|
|
if (!keepaliveDesired) return;
|
|
if (reconnectTimer) return;
|
|
|
|
reconnectTimer = setTimeout(() => {
|
|
reconnectTimer = null;
|
|
if (!initialized) return;
|
|
if (!keepaliveDesired) return;
|
|
if (!keepalivePort) {
|
|
console.log('[rr-keepalive] Attempting scheduled reconnect...');
|
|
keepalivePort = connectToBackground();
|
|
}
|
|
}, delayMs);
|
|
}
|
|
|
|
/**
|
|
* Create a Port connection to Background.
|
|
*/
|
|
function connectToBackground(): chrome.runtime.Port | null {
|
|
if (typeof chrome === 'undefined' || !chrome.runtime?.connect) {
|
|
console.warn('[rr-keepalive] chrome.runtime.connect not available');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const port = chrome.runtime.connect({ name: RR_V3_KEEPALIVE_PORT_NAME });
|
|
|
|
port.onMessage.addListener((msg: unknown) => {
|
|
if (!isKeepaliveMessage(msg)) return;
|
|
|
|
if (msg.type === 'keepalive.start') {
|
|
console.log('[rr-keepalive] Received start command via Port');
|
|
startPingLoop();
|
|
} else if (msg.type === 'keepalive.stop') {
|
|
console.log('[rr-keepalive] Received stop command via Port');
|
|
stopPingLoop();
|
|
} else if (msg.type === 'keepalive.pong') {
|
|
// Background replied to our ping.
|
|
console.debug('[rr-keepalive] Received pong');
|
|
}
|
|
});
|
|
|
|
port.onDisconnect.addListener(() => {
|
|
console.log('[rr-keepalive] Port disconnected');
|
|
keepalivePort = null;
|
|
// Only reconnect if keepalive is still desired.
|
|
scheduleReconnect(1000);
|
|
});
|
|
|
|
console.log('[rr-keepalive] Connected to background');
|
|
return port;
|
|
} catch (e) {
|
|
console.warn('[rr-keepalive] Failed to connect:', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ==================== Ping Loop ====================
|
|
|
|
/**
|
|
* Send a ping message to Background.
|
|
*/
|
|
function sendPing(): void {
|
|
if (!keepalivePort) {
|
|
keepalivePort = connectToBackground();
|
|
}
|
|
|
|
if (!keepalivePort) return;
|
|
|
|
const msg: KeepaliveMessage = {
|
|
type: 'keepalive.ping',
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
try {
|
|
keepalivePort.postMessage(msg);
|
|
console.debug('[rr-keepalive] Sent ping');
|
|
} catch (e) {
|
|
console.warn('[rr-keepalive] Failed to send ping:', e);
|
|
keepalivePort = null;
|
|
scheduleReconnect(1000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the ping loop.
|
|
*/
|
|
function startPingLoop(): void {
|
|
if (pingTimer) return;
|
|
|
|
keepaliveDesired = true;
|
|
|
|
// Ensure we have a Port connection.
|
|
if (!keepalivePort) {
|
|
keepalivePort = connectToBackground();
|
|
}
|
|
|
|
// Send one ping immediately.
|
|
sendPing();
|
|
|
|
// Start the interval timer.
|
|
pingTimer = setInterval(() => {
|
|
sendPing();
|
|
}, DEFAULT_KEEPALIVE_PING_INTERVAL_MS);
|
|
|
|
console.log(
|
|
`[rr-keepalive] Ping loop started (interval=${DEFAULT_KEEPALIVE_PING_INTERVAL_MS}ms)`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stop the ping loop.
|
|
* This must fully stop keepalive: no timer, no Port, and no reconnection attempts.
|
|
*/
|
|
function stopPingLoop(): void {
|
|
keepaliveDesired = false;
|
|
|
|
if (pingTimer) {
|
|
clearInterval(pingTimer);
|
|
pingTimer = null;
|
|
}
|
|
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
|
|
// Disconnect the Port to fully stop keepalive.
|
|
if (keepalivePort) {
|
|
try {
|
|
keepalivePort.disconnect();
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
keepalivePort = null;
|
|
}
|
|
|
|
console.log('[rr-keepalive] Ping loop stopped');
|
|
}
|
|
|
|
// ==================== Public API ====================
|
|
|
|
/**
|
|
* Initialize keepalive control handlers.
|
|
* @description Registers the runtime control listener and waits for start/stop commands.
|
|
*/
|
|
export function initKeepalive(): void {
|
|
if (initialized) return;
|
|
initialized = true;
|
|
|
|
// Check Chrome API availability.
|
|
if (typeof chrome === 'undefined' || !chrome.runtime?.onMessage) {
|
|
console.warn('[rr-keepalive] chrome.runtime.onMessage not available');
|
|
return;
|
|
}
|
|
|
|
// Listen for runtime control messages from Background.
|
|
// This allows Background to send start/stop even when Port is not connected.
|
|
chrome.runtime.onMessage.addListener((msg: unknown, _sender, sendResponse) => {
|
|
if (!isKeepaliveControlMessage(msg)) return;
|
|
|
|
if (msg.command === 'start') {
|
|
console.log('[rr-keepalive] Received runtime start command');
|
|
startPingLoop();
|
|
} else {
|
|
console.log('[rr-keepalive] Received runtime stop command');
|
|
stopPingLoop();
|
|
}
|
|
|
|
try {
|
|
sendResponse({ ok: true });
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
});
|
|
|
|
// Also establish initial Port connection for backwards compatibility.
|
|
if (chrome.runtime?.connect) {
|
|
keepalivePort = connectToBackground();
|
|
}
|
|
|
|
console.log('[rr-keepalive] Keepalive initialized');
|
|
}
|
|
|
|
/**
|
|
* Check whether keepalive is active.
|
|
*/
|
|
export function isKeepaliveActive(): boolean {
|
|
return keepaliveDesired && pingTimer !== null && keepalivePort !== null;
|
|
}
|
|
|
|
/**
|
|
* Get the active port count (for debugging).
|
|
* @deprecated Use isKeepaliveActive() instead
|
|
*/
|
|
export function getActivePortCount(): number {
|
|
return keepalivePort ? 1 : 0;
|
|
}
|
|
|
|
// Re-export for backwards compatibility
|
|
export {
|
|
RR_V3_KEEPALIVE_PORT_NAME,
|
|
type KeepaliveMessage,
|
|
} from '@/common/rr-v3-keepalive-protocol';
|