gitpilot / frontend /utils /appInit.js
github-actions[bot]
Deploy from cd8f9f9f
77051fb
/**
* GitPilot App Initialization β€” Single Source of Truth.
*
* Best-practice bootstrap pattern:
* - Runs EXACTLY ONCE per page load (even with React StrictMode)
* - Two-phase strategy: fast ping β†’ then parallel status fetch
* - Long retry budget for slow backends (WSL, HF Spaces cold start)
* - Shared result between App.jsx and LoginPage.jsx
* - No duplicate polling, no race conditions
*
* Phase 1 β€” Readiness probe (/api/ping):
* /api/ping is a zero-dependency endpoint that responds instantly
* once uvicorn is listening. We poll it with short timeouts and many
* retries to detect backend readiness WITHOUT wasting time on the
* heavy /api/status endpoint (which does GitHub API checks).
*
* Phase 2 β€” Data fetch (/api/status + /api/auth/status):
* Only after ping succeeds, we fetch the real status data in parallel.
* These can still be slow (GitHub API, LLM provider probes) but the
* user already sees the login page.
*
* API call budget per page load:
* - Best case: 2-3 Γ— /api/ping + 1 Γ— /api/status + 1 Γ— /api/auth/status
* - Worst case: up to 30 Γ— /api/ping + 1 + 1 (60s timeout budget)
*/
import { safeFetchJSON, apiUrl } from './api.js';
// Module-level singleton β€” survives React StrictMode double-mount
let _initPromise = null;
let _initResult = null;
const PING_MAX_ATTEMPTS = 30; // up to ~60s of readiness polling
const PING_INTERVAL_MS = 2000; // 2s between pings
const PING_TIMEOUT_MS = 4000; // each ping gives up after 4s
const STATUS_TIMEOUT_MS = 15000; // once ready, status fetch has 15s
/**
* Wait for the backend to become reachable by polling /api/ping.
* This is a zero-dependency endpoint that responds instantly once
* uvicorn is listening β€” much faster than /api/status which does
* GitHub API checks.
*
* @returns {Promise<boolean>} true if backend became reachable, false otherwise
*/
async function waitForBackend() {
for (let i = 0; i < PING_MAX_ATTEMPTS; i++) {
try {
const result = await safeFetchJSON(
apiUrl('/api/ping'),
{ timeout: PING_TIMEOUT_MS }
);
if (result && (result.ok === true || result.service)) {
console.log(
`[initApp] βœ… Backend reachable after ${i + 1} ping attempt(s) ` +
`(${(i * PING_INTERVAL_MS) / 1000}s elapsed)`
);
return true;
}
} catch (err) {
// Silent β€” we expect failures during cold start
if (i === 0 || i % 5 === 0) {
console.log(
`[initApp] Waiting for backend... ` +
`attempt ${i + 1}/${PING_MAX_ATTEMPTS}`
);
}
}
// Wait before next ping (except after last attempt)
if (i < PING_MAX_ATTEMPTS - 1) {
await new Promise((r) => setTimeout(r, PING_INTERVAL_MS));
}
}
return false;
}
/**
* Initialize the app.
* Phase 1: poll /api/ping until backend is reachable
* Phase 2: fetch /api/status and /api/auth/status in parallel
*
* @returns {Promise<{status: object|null, authMode: string, ready: boolean, error: string|null}>}
*/
export function initApp() {
if (_initPromise) {
return _initPromise;
}
_initPromise = (async () => {
// ── Phase 1: wait for backend to be reachable ──
const reachable = await waitForBackend();
if (!reachable) {
console.error(
`[initApp] ❌ Backend did not respond after ${PING_MAX_ATTEMPTS} ping attempts ` +
`(${(PING_MAX_ATTEMPTS * PING_INTERVAL_MS) / 1000}s). Giving up.`
);
_initResult = {
status: null,
authMode: 'device',
ready: false,
error: 'Backend did not become reachable. Please check that the server is running.',
};
return _initResult;
}
// ── Phase 2: fetch real data in parallel ──
try {
console.log('[initApp] Fetching /api/status + /api/auth/status in parallel...');
const [status, authStatus] = await Promise.all([
safeFetchJSON(apiUrl('/api/status'), { timeout: STATUS_TIMEOUT_MS }),
safeFetchJSON(apiUrl('/api/auth/status'), { timeout: STATUS_TIMEOUT_MS })
.catch(() => null),
]);
console.log('[initApp] βœ… Init complete');
_initResult = {
status,
authMode: (authStatus && authStatus.mode) || 'device',
ready: true,
error: null,
};
return _initResult;
} catch (err) {
// Backend was reachable via ping but status fetch failed
// Still return ready:true so UI can proceed with limited state
console.warn(
`[initApp] Status fetch failed after ping succeeded: ${err.message || err}. ` +
`Proceeding with limited state.`
);
_initResult = {
status: null,
authMode: 'device',
ready: true, // backend is up, just slow
error: null,
};
return _initResult;
}
})();
return _initPromise;
}
/**
* Get the cached init result (null if init hasn't completed yet).
*/
export function getInitResult() {
return _initResult;
}
/**
* Reset the init singleton. Call this only when you need to force
* a re-initialization (e.g., after the user manually clicks "Retry").
*/
export function resetInit() {
_initPromise = null;
_initResult = null;
}