Flips the connection model so sidecars register themselves with the API server via WebSocket at /api/sidecar/register, enabling dynamic discovery, location independence, and automatic reconnection from either side. - Add registration protocol types and PTY command/event types - Create sidecar-registry.ts (replaces sidecar-client.ts) as passive registry - Create sidecar connector (connect.ts) with exponential backoff reconnect - Convert process sidecar from WS server to WS client - Convert PTY sidecar from WS server to multiplexed WS client - Simplify terminal bridge to thin adapter using registry - Add PTY sidecar as PM2-managed process - Update all consumer imports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
3.4 KiB
TypeScript
136 lines
3.4 KiB
TypeScript
import type { SidecarCommand, SidecarEvent, PtyCommand, PtyEvent } from './protocol';
|
|
import type { SidecarRegistration, RegistrationAck } from './registration-protocol';
|
|
|
|
type AnyCommand = SidecarCommand | PtyCommand;
|
|
type AnyEvent = SidecarEvent | PtyEvent;
|
|
|
|
type SidecarConnectorConfig = {
|
|
apiUrl: string; // ws://127.0.0.1:5000/api/sidecar/register
|
|
name: string;
|
|
capabilities: string[];
|
|
onCommand: (cmd: AnyCommand, reply: (msg: AnyEvent) => void) => void;
|
|
onConnected?: () => void;
|
|
onDisconnected?: () => void;
|
|
};
|
|
|
|
type SidecarConnection = {
|
|
send: (msg: AnyEvent) => void;
|
|
destroy: () => void;
|
|
isConnected: () => boolean;
|
|
};
|
|
|
|
const RECONNECT_DELAYS = [200, 500, 1000, 2000, 4000, 8000, 15000];
|
|
|
|
export function createSidecarConnector(config: SidecarConnectorConfig): SidecarConnection {
|
|
let ws: WebSocket | null = null;
|
|
let connected = false;
|
|
let reconnectAttempt = 0;
|
|
let reconnectTimer: Timer | null = null;
|
|
let destroyed = false;
|
|
let sidecarId: string | null = null;
|
|
|
|
function connect() {
|
|
if (destroyed) return;
|
|
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) return;
|
|
|
|
try {
|
|
ws = new WebSocket(config.apiUrl);
|
|
} catch {
|
|
scheduleReconnect();
|
|
return;
|
|
}
|
|
|
|
ws.onopen = () => {
|
|
reconnectAttempt = 0;
|
|
|
|
// Send registration
|
|
const registration: SidecarRegistration = {
|
|
type: 'register',
|
|
name: config.name,
|
|
capabilities: config.capabilities,
|
|
};
|
|
ws!.send(JSON.stringify(registration));
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data as string);
|
|
|
|
// Handle registration ack
|
|
if (msg.type === 'registered') {
|
|
const ack = msg as RegistrationAck;
|
|
sidecarId = ack.id;
|
|
connected = true;
|
|
console.log(`[sidecar] registered with API server (id=${sidecarId})`);
|
|
config.onConnected?.();
|
|
return;
|
|
}
|
|
|
|
// Handle commands from API server
|
|
const reply = (response: AnyEvent) => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(response));
|
|
}
|
|
};
|
|
config.onCommand(msg as AnyCommand, reply);
|
|
} catch {
|
|
// skip malformed messages
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
const wasConnected = connected;
|
|
connected = false;
|
|
sidecarId = null;
|
|
ws = null;
|
|
if (wasConnected) {
|
|
console.log('[sidecar] disconnected from API server');
|
|
config.onDisconnected?.();
|
|
}
|
|
scheduleReconnect();
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
// onclose will fire after this
|
|
};
|
|
}
|
|
|
|
function scheduleReconnect() {
|
|
if (destroyed || reconnectTimer) return;
|
|
const delay = RECONNECT_DELAYS[Math.min(reconnectAttempt, RECONNECT_DELAYS.length - 1)]!;
|
|
reconnectAttempt++;
|
|
reconnectTimer = setTimeout(() => {
|
|
reconnectTimer = null;
|
|
connect();
|
|
}, delay);
|
|
}
|
|
|
|
function send(msg: AnyEvent): void {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(msg));
|
|
}
|
|
}
|
|
|
|
function destroy(): void {
|
|
destroyed = true;
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer);
|
|
reconnectTimer = null;
|
|
}
|
|
if (ws) {
|
|
ws.close();
|
|
ws = null;
|
|
}
|
|
connected = false;
|
|
}
|
|
|
|
// Start connecting
|
|
connect();
|
|
|
|
return {
|
|
send,
|
|
destroy,
|
|
isConnected: () => connected,
|
|
};
|
|
}
|