openpencil/src/app/automation/bridge/server.ts
2026-07-03 17:05:04 +03:00

99 lines
2.9 KiB
TypeScript

/**
* Browser-side automation handler.
*
* Connects to the bridge via WebSocket, receives RPC requests,
* executes them against the live EditorStore, and sends results back.
*/
import { AUTOMATION_WS_PORT } from '@open-pencil/core/constants'
import { randomHex } from '@open-pencil/core/random'
import { makeFigmaFromStore } from '@/app/automation/bridge/figma-factory'
import { createAutomationCommandHandlers } from '@/app/automation/bridge/handlers'
import type { EditorStore } from '@/app/editor/active-store'
export function connectAutomation(getStore: () => EditorStore, authToken: string | null = null) {
const token = authToken ?? randomHex(32)
let ws: WebSocket | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | undefined
let intentionalDisconnect = false
const { handleRequest: handleAutomationRequest } =
createAutomationCommandHandlers(makeFigmaFromStore)
async function handleRequest(_id: string, command: string, args: unknown): Promise<unknown> {
return handleAutomationRequest(getStore(), command, args)
}
function connect() {
try {
ws = new WebSocket(`ws://127.0.0.1:${AUTOMATION_WS_PORT}`)
} catch (e) {
console.error(
'[Automation] WebSocket constructor failed:',
e instanceof Error ? e.message : e
)
scheduleReconnect()
return
}
ws.onopen = () => {
console.debug('[Automation] WebSocket connected to MCP server')
ws?.send(JSON.stringify({ type: 'register', token }))
}
ws.onmessage = async (event) => {
try {
const msg = JSON.parse(event.data) as {
type: string
id: string
command: string
args?: unknown
}
if (msg.type !== 'request' || !msg.id) return
try {
const result = await handleRequest(msg.id, msg.command, msg.args)
ws?.send(JSON.stringify({ type: 'response', id: msg.id, ...(result as object) }))
} catch (e) {
ws?.send(
JSON.stringify({
type: 'response',
id: msg.id,
ok: false,
error: e instanceof Error ? e.message : String(e)
})
)
}
} catch (e) {
console.warn('Failed to parse WebSocket message:', e)
}
}
ws.onclose = (event) => {
ws = null
if (intentionalDisconnect || event.code === 1000) return
console.error('[Automation] WebSocket closed:', `code=${event.code} reason=${event.reason}`)
scheduleReconnect()
}
ws.onerror = (event) => {
console.error('[Automation] WebSocket error:', event)
ws?.close()
}
}
function scheduleReconnect() {
clearTimeout(reconnectTimer)
reconnectTimer = setTimeout(connect, 2000)
}
function disconnect() {
intentionalDisconnect = true
clearTimeout(reconnectTimer)
ws?.close()
ws = null
}
connect()
return { disconnect, token }
}