Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions extension/dist/background.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const DAEMON_PORT = 19825;
const DAEMON_HOST = "localhost";
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
const DAEMON_HTTP_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
const WS_RECONNECT_BASE_DELAY = 2e3;
const WS_RECONNECT_MAX_DELAY = 6e4;

Expand Down Expand Up @@ -94,6 +95,7 @@ function registerListeners() {
let ws = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
let connecting = false;
const _origLog = console.log.bind(console);
const _origWarn = console.warn.bind(console);
const _origError = console.error.bind(console);
Expand All @@ -117,38 +119,76 @@ console.error = (...args) => {
_origError(...args);
forwardLog("error", args);
};
function connect() {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
async function isDaemonReachable() {
try {
ws = new WebSocket(DAEMON_WS_URL);
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 2e3);
await fetch(DAEMON_HTTP_URL, { method: "HEAD", signal: ctrl.signal });
clearTimeout(timer);
return true;
} catch {
return false;
}
}
function connect() {
if (connecting) return;
if (ws) {
const state = ws.readyState;
if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) return;
if (state === WebSocket.CLOSING) return;
}
connecting = true;
isDaemonReachable().then((reachable) => {
if (!reachable) {
connecting = false;
ws = null;
scheduleReconnect();
return;
}
openWebSocket();
});
}
function openWebSocket() {
let socket;
try {
socket = new WebSocket(DAEMON_WS_URL);
} catch (err) {
connecting = false;
ws = null;
scheduleReconnect();
return;
}
ws.onopen = () => {
ws = socket;
connecting = false;
socket.onopen = () => {
console.log("[opencli] Connected to daemon");
reconnectAttempts = 0;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};
ws.onmessage = async (event) => {
socket.onmessage = async (event) => {
try {
const command = JSON.parse(event.data);
const result = await handleCommand(command);
ws?.send(JSON.stringify(result));
if (ws === socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(result));
}
} catch (err) {
console.error("[opencli] Message handling error:", err);
}
};
ws.onclose = () => {
socket.onclose = () => {
console.log("[opencli] Disconnected from daemon");
ws = null;
if (ws === socket) ws = null;
scheduleReconnect();
};
ws.onerror = () => {
ws?.close();
socket.onerror = () => {
try {
socket.close();
} catch {
}
};
}
function scheduleReconnect() {
Expand Down
79 changes: 67 additions & 12 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
*/

import type { Command, Result } from './protocol';
import { DAEMON_WS_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
import { DAEMON_WS_URL, DAEMON_HTTP_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
import * as cdp from './cdp';

let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectAttempts = 0;
let connecting = false;

// ─── Console log forwarding ──────────────────────────────────────────
// Hook console.log/warn/error to forward logs to daemon via WebSocket.
Expand All @@ -34,43 +35,97 @@ console.error = (...args: unknown[]) => { _origError(...args); forwardLog('error

// ─── WebSocket connection ────────────────────────────────────────────

/**
* Probe whether the daemon is reachable before opening a WebSocket.
* This avoids the noisy ERR_CONNECTION_REFUSED console error that Chrome
* prints for failed WebSocket connections (which cannot be suppressed).
*/
async function isDaemonReachable(): Promise<boolean> {
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 2000);
// A simple fetch to the daemon HTTP port — any response (even 404) means it's up.
await fetch(DAEMON_HTTP_URL, { method: 'HEAD', signal: ctrl.signal });
clearTimeout(timer);
return true;
} catch {
return false;
}
}

function connect(): void {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
// Guard against all in-progress or active states
if (connecting) return;
if (ws) {
const state = ws.readyState;
if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) return;
// CLOSING state — wait for onclose to fire, which will schedule reconnect
if (state === WebSocket.CLOSING) return;
}

connecting = true;

// Probe first, then connect — avoids ERR_CONNECTION_REFUSED noise
isDaemonReachable().then((reachable) => {
if (!reachable) {
connecting = false;
ws = null;
scheduleReconnect();
return;
}
openWebSocket();
});
}

function openWebSocket(): void {
let socket: WebSocket;
try {
ws = new WebSocket(DAEMON_WS_URL);
} catch {
socket = new WebSocket(DAEMON_WS_URL);
} catch (err) {
// WebSocket constructor can throw if the Service Worker is being
// terminated or the URL is unreachable at construction time.
connecting = false;
ws = null;
scheduleReconnect();
return;
}
ws = socket;
connecting = false;

ws.onopen = () => {
socket.onopen = () => {
console.log('[opencli] Connected to daemon');
reconnectAttempts = 0; // Reset on successful connection
reconnectAttempts = 0;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};

ws.onmessage = async (event) => {
socket.onmessage = async (event) => {
try {
const command = JSON.parse(event.data as string) as Command;
const result = await handleCommand(command);
ws?.send(JSON.stringify(result));
// Only send if this socket is still the active one and open
if (ws === socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(result));
}
} catch (err) {
console.error('[opencli] Message handling error:', err);
}
};

ws.onclose = () => {
socket.onclose = () => {
console.log('[opencli] Disconnected from daemon');
ws = null;
// Only clear if this is still the active socket (avoid race with a newer connect())
if (ws === socket) ws = null;
scheduleReconnect();
};

ws.onerror = () => {
ws?.close();
socket.onerror = () => {
// onerror is always followed by onclose, so just make sure the socket
// is closed cleanly. Wrap in try/catch since close() can throw if the
// Service Worker context is being torn down.
try { socket.close(); } catch { /* ignored */ }
};
}

Expand Down