// canonical bridge port for sootsim's WebSocket connection between the CLI // and the running engine. matches main — kept in a single module so CLI, // dev plugin, and runtime agree on the same value. export const DEFAULT_SOOTSIM_BRIDGE_PORT = 7668 export const DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/` function parsePort(value: unknown): number | null { if (typeof value === 'number') { return Number.isInteger(value) && value > 0 ? value : null } if (typeof value !== 'string') return null const trimmed = value.trim() if (!trimmed) return null const parsed = Number(trimmed) return Number.isInteger(parsed) && parsed > 0 ? parsed : null } function parsePortOffset(value: unknown): number { const parsed = parsePort(value) return parsed === null ? 0 : parsed } export function resolveSootsimBridgePort( input: { explicitPort?: unknown portOffset?: unknown } = {}, ): number { const explicitPort = parsePort(input.explicitPort) if (explicitPort !== null) return explicitPort return DEFAULT_SOOTSIM_BRIDGE_PORT + parsePortOffset(input.portOffset) } export function resolveSootsimBridgePortForHttpPort(input: { explicitPort?: unknown httpPort?: unknown baseHttpPort: number }): number { const explicitPort = parsePort(input.explicitPort) if (explicitPort !== null) return explicitPort const httpPort = parsePort(input.httpPort) const portOffset = httpPort === null ? 0 : Math.max(0, httpPort - input.baseHttpPort) return resolveSootsimBridgePort({ portOffset }) } const CONTRAST_DEV_HTTP_BASE_PORT = 3000 const SOOTSIM_SITE_DEV_HTTP_BASE_PORT = 3001 const SOOTSIM_SHELL_DEV_HTTP_BASE_PORT = 5173 const LOCAL_DEV_HTTP_BASE_PORTS = [ CONTRAST_DEV_HTTP_BASE_PORT, SOOTSIM_SHELL_DEV_HTTP_BASE_PORT, ] const MAX_LOCAL_DEV_PORT_OFFSET = 2000 function parseHrefUrl(value: unknown): URL | null { if (typeof value !== 'string' || !value.trim()) return null try { return new URL(value) } catch { return null } } function closestLocalDevBaseHttpPort(httpPort: number): number | null { let closest: { basePort: number; offset: number } | null = null for (const basePort of LOCAL_DEV_HTTP_BASE_PORTS) { const offset = httpPort - basePort if (offset < 0 || offset > MAX_LOCAL_DEV_PORT_OFFSET) continue if (!closest || offset < closest.offset) closest = { basePort, offset } } return closest?.basePort ?? null } /** the vite dev shell that shares a world with a bridge port. the two are one * offset apart by construction (bridge 7668 + N, shell 5173 + N), so a caller * that pinned an explicit bridge port can name its own shell directly. that * keeps explicit ports independent from default-world selection and lets a * page register on the exact bridge its caller will poll. null when the port * is not a local dev bridge port. */ export function resolveSootsimShellBaseUrlForBridgePort( bridgePort: number, ): string | null { const offset = bridgePort - DEFAULT_SOOTSIM_BRIDGE_PORT if (offset < 0 || offset > MAX_LOCAL_DEV_PORT_OFFSET) return null return `http://localhost:${SOOTSIM_SHELL_DEV_HTTP_BASE_PORT + offset}/` } export function resolveSootsimBridgePortForRuntimeLocation( input: { explicitPort?: unknown href?: unknown httpPort?: unknown } = {}, ): number { const explicitPort = parsePort(input.explicitPort) if (explicitPort !== null) return explicitPort const hrefUrl = parseHrefUrl(input.href) const httpPort = parsePort(input.httpPort) ?? parsePort(hrefUrl?.port) if (httpPort === null) return DEFAULT_SOOTSIM_BRIDGE_PORT if ( hrefUrl?.pathname.startsWith('/sootsim') && (hrefUrl.searchParams.has('embed') || hrefUrl.searchParams.has('nativePreview')) ) { return resolveSootsimBridgePortForHttpPort({ httpPort, baseHttpPort: CONTRAST_DEV_HTTP_BASE_PORT, }) } if ( hrefUrl?.pathname.startsWith('/sootsim') && httpPort >= SOOTSIM_SITE_DEV_HTTP_BASE_PORT && httpPort <= SOOTSIM_SITE_DEV_HTTP_BASE_PORT + MAX_LOCAL_DEV_PORT_OFFSET ) { return resolveSootsimBridgePortForHttpPort({ httpPort, baseHttpPort: SOOTSIM_SITE_DEV_HTTP_BASE_PORT, }) } const baseHttpPort = closestLocalDevBaseHttpPort(httpPort) if (baseHttpPort === null) return DEFAULT_SOOTSIM_BRIDGE_PORT return resolveSootsimBridgePortForHttpPort({ httpPort, baseHttpPort }) } export function resolveSootsimWebSocketProxyUrl(input: { targetUrl: string href: string }): string { const pageUrl = parseHrefUrl(input.href) if (!pageUrl || (pageUrl.protocol !== 'http:' && pageUrl.protocol !== 'https:')) { throw new Error(`invalid rnx websocket proxy page URL: ${input.href}`) } let targetUrl: URL try { targetUrl = new URL(input.targetUrl) } catch { throw new Error(`invalid rnx websocket proxy target URL: ${input.targetUrl}`) } if (targetUrl.protocol !== 'ws:' && targetUrl.protocol !== 'wss:') { throw new Error(`invalid rnx websocket proxy target URL: ${input.targetUrl}`) } const proxyUrl = new URL('/__websocket-proxy', pageUrl) proxyUrl.protocol = pageUrl.protocol === 'https:' ? 'wss:' : 'ws:' proxyUrl.searchParams.set('url', input.targetUrl) return proxyUrl.href } export function resolveSootsimBridgeWebSocketUrl(input: { bridgePort: number href: string }): string { // 127.0.0.1, not localhost. the daemon binds ipv4 only (bridge-host bindOnce) // so that localhost's ::1/127.0.0.1 coinflip cannot hide it; the health probe // already talks to 127.0.0.1. targeting localhost here undoes that: when node // tries ::1 first and does not fall back, vite's proxy answers 502 and the // browser logs a console error that fails e2e. return resolveSootsimWebSocketProxyUrl({ targetUrl: `ws://127.0.0.1:${input.bridgePort}`, href: input.href, }) } export function isSootsimBridgeHealth(value: unknown, expectedPort: number): boolean { return ( typeof value === 'object' && value !== null && Reflect.get(value, 'ok') === true && Reflect.get(value, 'bridgePort') === expectedPort ) } // private websocket close code used when the daemon intentionally closes a // sim. browser clients treat it as terminal and skip their normal reconnect // loop; playwright-owned sims use it to close their hosting context. export const SOOTSIM_BRIDGE_SIM_CLOSE_CODE = 4001 export const SOOTSIM_BRIDGE_SIM_CLOSE_REASON = 'sootsim close'