import { createServer } from 'node:net' import { dirname, join } from 'node:path' import { BrowserWindow, PATHS } from 'electrobun/bun' const DEV_SERVER_URL = 'http://localhost:3000' const SERVER_READY_TIMEOUT_MS = 5_000 const MAX_SPAWN_RETRIES = 3 const isDev = (): boolean => { const env = process.env.ELECTROBUN_BUILD_ENV return !env || env === 'dev' } const getServerEntryPath = (): string => { return join(PATHS.VIEWS_FOLDER, '..', 'server-output', 'server', 'index.mjs') } const getFreePort = (): Promise => { return new Promise((resolve, reject) => { const srv = createServer() srv.unref() srv.once('error', reject) srv.listen({ port: 0, host: '127.0.0.1', exclusive: true }, () => { const addr = srv.address() if (addr && typeof addr === 'object') { const port = addr.port srv.close((err) => (err ? reject(err) : resolve(port))) } else { srv.close(() => reject(new Error('Unexpected address() result'))) } }) }) } const waitForServer = async ( url: string, timeoutMs = SERVER_READY_TIMEOUT_MS, ): Promise => { const start = Date.now() while (Date.now() - start < timeoutMs) { try { const response = await fetch(url, { method: 'HEAD' }) if (response.ok) return true } catch (_) { // Server not up yet, retry after sleep } await Bun.sleep(100) } return false } const spawnServer = async (): Promise<{ process: ReturnType url: string }> => { const serverEntryPath = getServerEntryPath() const serverDir = dirname(serverEntryPath) for (let attempt = 1; attempt <= MAX_SPAWN_RETRIES; attempt++) { const port = await getFreePort() const url = `http://127.0.0.1:${port}` const serverProc = Bun.spawn([process.execPath, serverEntryPath], { cwd: serverDir, env: { ...process.env, PORT: String(port), HOST: '127.0.0.1', }, stdio: ['ignore', 'inherit', 'inherit'], }) const ready = await Promise.race([ waitForServer(url), serverProc.exited.then((code) => { throw new Error(`Server exited with code ${code} before becoming ready`) }), ]) if (ready) { return { process: serverProc, url } } serverProc.kill() await serverProc.exited console.warn( `Server failed to become ready on port ${port} (attempt ${attempt}/${MAX_SPAWN_RETRIES})`, ) } throw new Error(`Server failed to start after ${MAX_SPAWN_RETRIES} attempts`) } const main = async () => { console.log('Starting Furtherverse Desktop...') let serverUrl: string let serverProcess: ReturnType | null = null if (isDev()) { console.log('Dev mode: waiting for external server at', DEV_SERVER_URL) const ready = await waitForServer(DEV_SERVER_URL) if (!ready) { console.error( 'Dev server not responding. Make sure to run: bun dev in apps/server', ) process.exit(1) } console.log('Dev server ready!') serverUrl = DEV_SERVER_URL } else { console.log('Production mode: starting embedded server...') try { const server = await spawnServer() serverProcess = server.process serverUrl = server.url console.log('Server ready at', serverUrl) } catch (err) { console.error('Failed to start embedded server:', err) process.exit(1) } } new BrowserWindow({ title: 'Furtherverse', url: serverUrl, frame: { x: 100, y: 100, width: 1200, height: 800, }, renderer: 'cef', }) if (serverProcess) { const cleanup = () => { if (serverProcess) { serverProcess.kill() serverProcess = null } } process.on('exit', cleanup) process.on('SIGTERM', () => { cleanup() process.exit(0) }) process.on('SIGINT', () => { cleanup() process.exit(0) }) serverProcess.exited.then((code) => { if (serverProcess) { console.error(`Server exited unexpectedly with code ${code}`) process.exit(1) } }) } } main().catch((error) => { console.error('Failed to start:', error) process.exit(1) })