'use client'; import React from 'react'; import { getStatus, listSessions, wsManager } from '@/lib/api'; import { useChatStore } from '@/lib/store'; import type { ProcessWsEvent, SessionUpdatedEvent, WsEvent } from '@/types'; function scheduleWhenIdle(task: () => void, timeout = 1200): () => void { if (typeof window === 'undefined') { task(); return () => {}; } const idleWindow = window as Window & typeof globalThis & { requestIdleCallback?: (callback: IdleRequestCallback, options?: IdleRequestOptions) => number; cancelIdleCallback?: (handle: number) => void; }; if (typeof idleWindow.requestIdleCallback === 'function') { const id = idleWindow.requestIdleCallback(() => task(), { timeout }); return () => idleWindow.cancelIdleCallback?.(id); } const id = globalThis.setTimeout(task, 250); return () => globalThis.clearTimeout(id); } function isProcessEvent(data: WsEvent | Record): data is ProcessWsEvent { const type = typeof data.type === 'string' ? data.type : ''; return type.startsWith('process_') || type === 'process_cancel_ack'; } function isSessionUpdatedEvent(data: WsEvent | Record): data is SessionUpdatedEvent { return data.type === 'session_updated' && typeof data.session_id === 'string'; } export function AppRuntimeBridge() { const sessionId = useChatStore((state) => state.sessionId); const setSessions = useChatStore((state) => state.setSessions); const setWsStatus = useChatStore((state) => state.setWsStatus); const setNanobotReady = useChatStore((state) => state.setNanobotReady); const resetProcessState = useChatStore((state) => state.resetProcessState); const ingestProcessEvent = useChatStore((state) => state.ingestProcessEvent); const statusCheckCleanupRef = React.useRef<(() => void) | null>(null); const statusCheckInFlightRef = React.useRef(false); const loadSessions = React.useCallback(async () => { try { const sessions = await listSessions(); setSessions(sessions); } catch { // backend may still be offline during first render } }, [setSessions]); const scheduleStatusCheck = React.useCallback(() => { if (statusCheckInFlightRef.current) return; statusCheckCleanupRef.current?.(); statusCheckCleanupRef.current = scheduleWhenIdle(async () => { statusCheckInFlightRef.current = true; try { await getStatus(); setNanobotReady(true); } catch { setNanobotReady(false); } finally { statusCheckInFlightRef.current = false; } }); }, [setNanobotReady]); React.useEffect(() => { void loadSessions(); }, [loadSessions]); React.useEffect(() => { resetProcessState(); const wsSessionId = sessionId.startsWith('web:') ? sessionId.slice(4) : sessionId; wsManager.connect(wsSessionId); }, [resetProcessState, sessionId]); React.useEffect(() => { const unsubStatus = wsManager.onStatusChange((status) => { setWsStatus(status); if (status === 'connected') { scheduleStatusCheck(); } else { statusCheckCleanupRef.current?.(); statusCheckCleanupRef.current = null; setNanobotReady(null); } }); return () => { statusCheckCleanupRef.current?.(); statusCheckCleanupRef.current = null; unsubStatus(); }; }, [scheduleStatusCheck, setNanobotReady, setWsStatus]); React.useEffect(() => { const unsubMessage = wsManager.onMessage((data) => { if (isSessionUpdatedEvent(data)) { void loadSessions(); return; } if (isProcessEvent(data)) { ingestProcessEvent(data); } }); return () => { unsubMessage(); }; }, [ingestProcessEvent, loadSessions]); return null; }