/** * Mémoire WebSocket Server — Multi-instance bridge server. * * Each Mémoire engine instance gets its own port (9223-9232). * Multiple Figma plugin instances can connect to the same server. * Supports chat relay, command dispatch, and real-time events. */ import { WebSocket } from "ws"; import { EventEmitter } from "events"; import type { MemoireEvent } from "../engine/core.js"; import type { AgentBoxState } from "../plugin/shared/contracts.js"; export interface BridgeClient { id: string; ws: WebSocket; file: string; fileKey: string; editor: string; connectedAt: Date; lastPing: Date; } export interface MemoireWsServerConfig { port?: number; instanceName?: string; studioUrl?: string; runtimeUrl?: string; onCommand?: (method: string, params: Record) => Promise; onChat?: (text: string, fromPlugin: string) => void; onEvent?: (event: MemoireEvent) => void; } /** Connection state from the server's perspective. */ export type ConnectionState = "connected" | "reconnecting" | "disconnected"; export declare class MemoireWsServer extends EventEmitter { private config; private wss; private clients; private rateLimits; private port; private _running; private clientCounter; private healthInterval; private pendingCommands; /** Tracks in-flight idempotent reads to prevent duplicate requests. method → commandId */ private inFlightMethods; private commandId; private _reconnectAttempts; private _isReconnecting; private _reconnectTimer; private _lastConnectedAt; private _lastDisconnectedAt; private readonly studioUrl; private readonly runtimeUrl; constructor(config?: MemoireWsServerConfig); get running(): boolean; get activePort(): number; get connectedClients(): BridgeClient[]; /** Number of consecutive reconnect attempts since last successful connection. */ get reconnectAttempts(): number; /** Whether the server is in the reconnect-wait state. */ get isReconnecting(): boolean; /** ISO timestamp of the last time a plugin connected successfully. */ get lastConnectedAt(): Date | null; /** ISO timestamp of the last time all plugins disconnected. */ get lastDisconnectedAt(): Date | null; /** * Current logical connection state based on client count and reconnect status. */ getConnectionState(): ConnectionState; /** * Start the WebSocket server, scanning ports BRIDGE_PORT_START-BRIDGE_PORT_END * for an available one. Warns if a port in the range is bound by a foreign process. */ start(preferPort?: number): Promise; /** * Stop the server and disconnect all clients. */ stop(): void; /** Idempotent read commands that should be deduplicated when already in-flight. */ private static readonly DEDUP_METHODS; /** * Send a command to the first connected Figma plugin and wait for response. */ sendCommand(method: string, params?: Record, timeout?: number): Promise; /** * Send a chat message to all connected Figma plugins. */ sendChat(text: string): void; /** * Send an event notification to all plugins. */ sendEvent(event: MemoireEvent): void; /** * Broadcast agent-box state so non-canvas consumers can observe orchestration progress. */ sendAgentStatus(status: AgentBoxState): void; /** * Broadcast data to all connected clients. */ broadcast(data: unknown): void; /** * Get status summary for all connections. */ getStatus(): { running: boolean; port: number; bridgeStatus: "stopped" | "running"; pluginStatus: "disconnected" | "connected"; clients: { id: string; file: string; fileKey: string; editor: string; connectedAt: string; lastPing: string; }[]; connectionState: ConnectionState; reconnectAttempts: number; lastConnectedAt: string | null; lastDisconnectedAt: string | null; }; /** * Check bridge health — pings the active plugin and measures round-trip latency. * Returns gracefully even when no plugin is connected. */ checkHealth(): Promise<{ connected: boolean; clientCount: number; latencyMs: number | null; uptime: number; }>; private startOnPort; private setupServer; private handleMessage; /** Check and update rate limit for a client. Returns true if allowed. */ private checkRateLimit; private getActiveClient; /** * Exponential backoff reconnect-wait loop. * * The server itself remains open and accepting connections — this just emits * diagnostic events at increasing intervals so operators can observe the * "waiting for plugin to come back" state without log spam. * * After RECONNECT_MAX_ATTEMPTS it emits an error event and stops the loop. */ private _scheduleReconnectWarn; private emitEvent; }