Files
platform/src/servers/sidecar/connect.ts
T
pastilhasandClaude Opus 4.6 a0d9ea63f9 sidecar self-registration: sidecars connect to API server instead of vice versa
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>
2026-03-06 08:48:19 +00:00

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,
};
}