velxio/frontend/src/services/compilation.ts

131 lines
3.9 KiB
TypeScript
Raw Normal View History

import axios from 'axios';
const API_BASE = import.meta.env.VITE_API_BASE || '/api';
export interface SketchFile {
name: string;
content: string;
}
export interface CompileResult {
success: boolean;
hex_content?: string;
binary_content?: string; // base64-encoded .bin for RP2040
binary_type?: 'bin' | 'uf2';
has_wifi?: boolean; // True when sketch uses WiFi (ESP32 only)
stdout: string;
stderr: string;
error?: string;
core_install_log?: string;
}
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
interface CompileStartResponse {
job_id: string;
}
interface CompileStatusResponse {
state: 'pending' | 'running' | 'done' | 'error';
started_at: number;
finished_at: number | null;
result: CompileResult | null;
error: string | null;
}
const POLL_INTERVAL_MS = 2000;
const MAX_POLL_DURATION_MS = 15 * 60 * 1000; // 15 minutes — covers cold ESP-IDF builds
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/**
* Compile a sketch via the async job pipeline.
*
* POST /compile/start { job_id }
* GET /compile/status/<job_id> (×N) { state, result?, error? }
*
* Each individual request returns in milliseconds, so Cloudflare's 100s edge
* timeout never kicks in even when the underlying ESP-IDF cold build runs
* for 5-7 minutes. Falls back to throwing an Error after MAX_POLL_DURATION_MS.
*/
export async function compileCode(
files: SketchFile[],
board: string = 'arduino:avr:uno',
2026-04-26 05:46:52 +07:00
projectId?: string | null,
): Promise<CompileResult> {
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
console.log('Sending compilation request to:', `${API_BASE}/compile/start`);
console.log('Board:', board);
console.log(
'Files:',
files.map((f) => f.name),
);
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
let jobId: string;
try {
const startResp = await axios.post<CompileStartResponse>(
`${API_BASE}/compile/start`,
2026-04-26 05:46:52 +07:00
{ files, board_fqbn: board, project_id: projectId ?? null },
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
{ withCredentials: true, timeout: 30000 },
);
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
jobId = startResp.data.job_id;
console.log('[compile] queued job', jobId);
} catch (error) {
console.error('Compilation request failed:', error);
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
if (axios.isAxiosError(error) && error.response) {
// Server returned a structured error (422, 500, etc.) — surface as a
// failed CompileResult so the editor can show stderr/error.
return error.response.data as CompileResult;
}
throw error instanceof Error
? error
: new Error('No response from server. Is the backend running?');
}
const startedAt = Date.now();
// Initial small delay so we don't hit /status before the background task
// has even moved past 'pending'.
await sleep(500);
while (true) {
if (Date.now() - startedAt > MAX_POLL_DURATION_MS) {
throw new Error(
`Compile timed out client-side after ${Math.round(MAX_POLL_DURATION_MS / 1000)}s`,
);
}
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
let status: CompileStatusResponse;
try {
const resp = await axios.get<CompileStatusResponse>(
`${API_BASE}/compile/status/${jobId}`,
{ withCredentials: true, timeout: 30000 },
);
status = resp.data;
} catch (error) {
// Transient poll error — log, wait, retry. Only abort on 404 (job
// expired or never existed).
if (axios.isAxiosError(error) && error.response?.status === 404) {
throw new Error(`Compile job ${jobId} not found (server may have restarted)`);
}
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
console.warn('[compile] status poll error, retrying:', error);
await sleep(POLL_INTERVAL_MS);
continue;
}
if (status.state === 'done' && status.result) {
const elapsed = Math.round((Date.now() - startedAt) / 1000);
console.log(`[compile] job ${jobId} done in ${elapsed}s`);
return status.result;
}
if (status.state === 'error') {
console.error(`[compile] job ${jobId} errored:`, status.error);
return {
success: false,
stdout: '',
stderr: '',
error: status.error || 'Compile failed',
};
}
feat(compile): async compile + status polling — no more 524 timeouts The synchronous /api/compile endpoint forced one long-lived HTTP request to span the entire build. Cloudflare's 100s edge timeout cuts that off mid-flight for any cold ESP-IDF compile (BMP280 takes 5-7 min on first run). The user-visible symptom was HTTP 524 well before the backend even noticed. Backend (compile.py) - New `POST /api/compile/start` returns `{job_id}` immediately and spawns the actual compile as an asyncio.create_task background. - New `GET /api/compile/status/{job_id}` returns the current job state (`pending` | `running` | `done` | `error`). Each poll completes in milliseconds, far under any edge timeout. - Existing `POST /api/compile/` kept verbatim for backward compatibility (AVR/RP2040 builds finish in seconds and don't trip 524). - Build logic extracted into `_run_compile()` so both paths share one implementation; no duplicated ESP-IDF / arduino-cli branching. - Async path opens its own short-lived DB session via AsyncSessionLocal for metric recording — the request-scoped session is dead by the time the background task finishes. - COMPILE_JOBS dict purges entries 30 minutes after completion so a busy server doesn't grow unboundedly. Frontend (compilation.ts) - compileCode() now: POST /compile/start → poll /compile/status every 2s until state ∈ {done, error}, with a 15-minute client-side cap. - 30s axios timeout per individual call (not per build) so transient network blips during a long compile auto-retry instead of failing. - 404 on /status throws (job expired / server restarted); other poll errors warn and retry. Surfaces structured error responses verbatim so the editor's compile-error panel keeps working unchanged. Limitation: COMPILE_JOBS lives in-process; if velxio ever scales to multiple FastAPI workers this needs to move to Redis or sqlite. Single- instance is fine today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:22:09 +07:00
await sleep(POLL_INTERVAL_MS);
}
}