From fe2a003e940fed7182db802b47afa1b027862c2c Mon Sep 17 00:00:00 2001 From: talksik Date: Tue, 14 Apr 2026 07:58:45 -0700 Subject: [PATCH] fix: huddles not launching reliably Closes #160 Race condition preventing proper initialization. --- js/src/electron.d.ts | 1 - js/src/huddle_window/HuddleApp.tsx | 17 +++++++------ js/src/main.ts | 40 +++++++++++++++--------------- js/src/preload.ts | 5 ---- 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/js/src/electron.d.ts b/js/src/electron.d.ts index 99fd6a3..c09ed0a 100644 --- a/js/src/electron.d.ts +++ b/js/src/electron.d.ts @@ -19,7 +19,6 @@ declare global { closeHuddle: () => void; }; electronHuddle: { - onConnect: (callback: (data: { token: string; serverUrl: string }) => void) => () => void; getScreenSources: () => Promise; }; electronAutoplay: { diff --git a/js/src/huddle_window/HuddleApp.tsx b/js/src/huddle_window/HuddleApp.tsx index 94d8b24..6ce85ff 100644 --- a/js/src/huddle_window/HuddleApp.tsx +++ b/js/src/huddle_window/HuddleApp.tsx @@ -24,14 +24,15 @@ import { RoomEvent, Track } from 'livekit-client'; import { useState, useEffect, useRef, useCallback } from 'react'; import { ScreenSourcePicker } from '@/components/screen-source-picker'; -export function HuddleApp() { - const [connection, setConnection] = useState<{ token: string; serverUrl: string } | null>(null); +function readConnectionFromHash(): { token: string; serverUrl: string } | null { + const params = new URLSearchParams(window.location.hash.slice(1)); + const token = params.get('token'); + const serverUrl = params.get('serverUrl'); + return token && serverUrl ? { token, serverUrl } : null; +} - useEffect(() => { - return window.electronHuddle.onConnect((data) => { - setConnection(data); - }); - }, []); +export function HuddleApp() { + const [connection] = useState(readConnectionFromHash); const handleDisconnected = () => { window.electronWindow.closeHuddle(); @@ -40,7 +41,7 @@ export function HuddleApp() { if (!connection) { return (
-

Connecting to huddle...

+

Missing huddle connection data.

); } diff --git a/js/src/main.ts b/js/src/main.ts index 4516720..9c3ed10 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -162,10 +162,7 @@ const createAutoplayWindow = () => { }; const createHuddleWindow = () => { - if (huddleWindow) { - huddleWindow.focus(); - return; - } + if (huddleWindow) return; huddleWindow = new BrowserWindow({ width: 1024, @@ -180,19 +177,28 @@ const createHuddleWindow = () => { }); hardenWindow(huddleWindow); - if (HUDDLE_WINDOW_VITE_DEV_SERVER_URL) { - huddleWindow.loadURL(HUDDLE_WINDOW_VITE_DEV_SERVER_URL); - } else { - huddleWindow.loadFile( - path.join(__dirname, `../renderer/${HUDDLE_WINDOW_VITE_NAME}/index.html`), - ); - } - huddleWindow.on('closed', () => { huddleWindow = null; }); }; +// Connection data is passed via URL hash so it's available synchronously on +// renderer mount — avoids the IPC race where `huddle:connect` could be sent +// before React attached its listener. +const loadHuddleWindow = (data: { token: string; serverUrl: string }) => { + if (!huddleWindow) return; + const params = new URLSearchParams({ token: data.token, serverUrl: data.serverUrl }); + const hash = params.toString(); + if (HUDDLE_WINDOW_VITE_DEV_SERVER_URL) { + huddleWindow.loadURL(`${HUDDLE_WINDOW_VITE_DEV_SERVER_URL}#${hash}`); + } else { + huddleWindow.loadFile( + path.join(__dirname, `../renderer/${HUDDLE_WINDOW_VITE_NAME}/index.html`), + { hash }, + ); + } +}; + function positionAutoplayWindow() { if (!autoplayWindow) return; const { width } = screen.getPrimaryDisplay().workAreaSize; @@ -261,14 +267,8 @@ ipcMain.on('window:fullscreen', (event) => { // Secondary window IPC handlers ipcMain.on('window:open-huddle', (_event, data: { token: string; serverUrl: string }) => { createHuddleWindow(); - // Send connection data once the huddle window is ready - huddleWindow?.webContents.once('did-finish-load', () => { - huddleWindow?.webContents.send('huddle:connect', data); - }); - // If already loaded, send immediately - if (!huddleWindow?.webContents.isLoading()) { - huddleWindow?.webContents.send('huddle:connect', data); - } + loadHuddleWindow(data); + huddleWindow?.focus(); }); ipcMain.on('window:close-huddle', () => { huddleWindow?.close(); diff --git a/js/src/preload.ts b/js/src/preload.ts index c54a084..137a7a8 100644 --- a/js/src/preload.ts +++ b/js/src/preload.ts @@ -12,11 +12,6 @@ contextBridge.exposeInMainWorld('electronWindow', { }); contextBridge.exposeInMainWorld('electronHuddle', { - onConnect: (callback: (data: { token: string; serverUrl: string }) => void) => { - const handler = (_event: Electron.IpcRendererEvent, data: { token: string; serverUrl: string }) => callback(data); - ipcRenderer.on('huddle:connect', handler); - return () => { ipcRenderer.removeListener('huddle:connect', handler); }; - }, getScreenSources: () => ipcRenderer.invoke('screen:get-sources'), });