| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { useEffect, useRef, useCallback } from 'react' |
| import useAppStore from '../store/appStore' |
|
|
| const POLL_INTERVAL_MS = 2000 |
| const POLLING_STATES = new Set(['installing', 'starting', 'stopping']) |
|
|
| |
| export function useProcessesConnection() { |
| const projectId = useAppStore((s) => s.project.id) |
| const status = useAppStore((s) => s.runtime.status) |
| const setRuntime = useAppStore((s) => s.setRuntime) |
| const appendLog = useAppStore((s) => s.appendLog) |
|
|
| const wsRef = useRef(null) |
| const pollRef = useRef(null) |
|
|
| |
| useEffect(() => { |
| if (!projectId) return |
|
|
| |
| |
| |
| |
| |
| |
| let cancelled = false |
| let reconnectTimer = null |
| let consecutiveFails = 0 |
|
|
| function connect() { |
| if (cancelled) return |
| wsRef.current?.close() |
| const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' |
| const ws = new WebSocket( |
| `${proto}://${window.location.host}/api/projects/${projectId}/logs` |
| ) |
| let opened = false |
| ws.onopen = () => { |
| opened = true |
| consecutiveFails = 0 |
| } |
| ws.onmessage = (e) => { |
| try { |
| const msg = JSON.parse(e.data) |
| if (msg.kind !== 'ping' && msg.line !== undefined) { |
| appendLog({ kind: msg.kind, line: msg.line }) |
| } |
| } catch { } |
| } |
| ws.onclose = () => { |
| if (cancelled) return |
| if (useAppStore.getState().project.id !== projectId) return |
| |
| |
| |
| if (!opened) { |
| consecutiveFails += 1 |
| if (consecutiveFails >= 5) return |
| const delay = Math.min(15_000, 1000 * 2 ** (consecutiveFails - 1)) |
| reconnectTimer = setTimeout(connect, delay) |
| } else { |
| reconnectTimer = setTimeout(connect, 2000) |
| } |
| } |
| wsRef.current = ws |
| } |
|
|
| connect() |
| return () => { |
| cancelled = true |
| if (reconnectTimer) clearTimeout(reconnectTimer) |
| wsRef.current?.close() |
| wsRef.current = null |
| } |
| }, [projectId, appendLog]) |
|
|
| |
| useEffect(() => { |
| if (!projectId || !POLLING_STATES.has(status)) return |
|
|
| pollRef.current = setInterval(async () => { |
| try { |
| const res = await fetch(`/api/projects/${projectId}/processes`) |
| if (!res.ok) return |
| const data = await res.json() |
| const { overall, frontend, backend } = data |
| const prev = useAppStore.getState().runtime |
| setRuntime({ |
| status: overall, |
| frontendStatus: frontend.status, |
| backendStatus: backend.status, |
| frontendPort: frontend.port ?? prev.frontendPort, |
| backendPort: backend.port ?? prev.backendPort, |
| }) |
| } catch { } |
| }, POLL_INTERVAL_MS) |
|
|
| return () => { |
| clearInterval(pollRef.current) |
| pollRef.current = null |
| } |
| }, [projectId, status, setRuntime]) |
| } |
|
|
| |
| export function useProcesses() { |
| const projectId = useAppStore((s) => s.project.id) |
| const status = useAppStore((s) => s.runtime.status) |
| const setRuntime = useAppStore((s) => s.setRuntime) |
| const clearLogs = useAppStore((s) => s.clearLogs) |
|
|
| const start = useCallback(async () => { |
| if (!projectId) return |
| clearLogs() |
| setRuntime({ status: 'installing' }) |
| try { |
| const res = await fetch(`/api/projects/${projectId}/processes/start`, { method: 'POST' }) |
| const data = await res.json() |
| if (!res.ok) { |
| setRuntime({ status: 'error' }) |
| return |
| } |
| setRuntime({ |
| status: data.overall === 'running' ? 'running' : 'starting', |
| frontendPort: data.frontend_port, |
| backendPort: data.backend_port, |
| }) |
| } catch { |
| setRuntime({ status: 'error' }) |
| } |
| }, [projectId, clearLogs, setRuntime]) |
|
|
| const stop = useCallback(async () => { |
| if (!projectId) return |
| setRuntime({ status: 'stopping' }) |
| try { |
| await fetch(`/api/projects/${projectId}/processes/stop`, { method: 'POST' }) |
| } catch { } |
| setRuntime({ |
| status: 'idle', |
| frontendPort: null, |
| backendPort: null, |
| frontendStatus: 'idle', |
| backendStatus: 'idle', |
| }) |
| }, [projectId, setRuntime]) |
|
|
| const reload = useCallback(() => { |
| const k = useAppStore.getState().runtime.iframeKey ?? 0 |
| setRuntime({ iframeKey: k + 1 }) |
| }, [setRuntime]) |
|
|
| return { start, stop, reload, status } |
| } |
|
|