Spaces:
Paused
Paused
| // Thin fetch wrapper + job polling for the FastAPI backend. | |
| import { saveBundle, deleteBundle } from './persist' | |
| async function request(path, options = {}) { | |
| const res = await fetch(path, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| ...options, | |
| }) | |
| if (!res.ok) { | |
| let detail = res.statusText | |
| try { | |
| const body = await res.json() | |
| detail = body.detail || JSON.stringify(body) | |
| } catch { /* keep statusText */ } | |
| throw new Error(detail) | |
| } | |
| return res.json() | |
| } | |
| // --- client-side persistence hooks ----------------------------------------- | |
| // After any mutation touching a project, refresh that project's cached bundle | |
| // in the browser (debounced, so a burst of edits makes one export). See | |
| // persist.js. `import`/`export` are excluded so we never recurse. | |
| const PROJECT_RE = /^\/api\/projects\/([^/]+)/ | |
| const _syncTimers = {} | |
| function projectIdFromPath(path) { | |
| const m = PROJECT_RE.exec(path) | |
| const id = m && m[1] | |
| return id && id !== 'import' ? id : null | |
| } | |
| async function syncProjectBundle(projectId) { | |
| try { | |
| saveBundle(await request(`/api/projects/${projectId}/export`)) | |
| } catch { /* project may have just been deleted — ignore */ } | |
| } | |
| function scheduleBundleSync(projectId) { | |
| clearTimeout(_syncTimers[projectId]) | |
| _syncTimers[projectId] = setTimeout(() => { | |
| delete _syncTimers[projectId] | |
| syncProjectBundle(projectId) | |
| }, 700) | |
| } | |
| async function mutate(path, options) { | |
| const result = await request(path, options) | |
| const pid = projectIdFromPath(path) | |
| if (pid) { | |
| // DELETE of the whole project (no trailing segment) drops its cache; | |
| // any other mutation re-syncs the bundle. | |
| if (options.method === 'DELETE' && path.replace(/\/$/, '') === `/api/projects/${pid}`) { | |
| deleteBundle(pid) | |
| } else { | |
| scheduleBundleSync(pid) | |
| } | |
| } | |
| return result | |
| } | |
| export const api = { | |
| get: (path) => request(path), | |
| post: (path, body) => mutate(path, { method: 'POST', body: JSON.stringify(body ?? {}) }), | |
| put: (path, body) => mutate(path, { method: 'PUT', body: JSON.stringify(body) }), | |
| patch: (path, body) => mutate(path, { method: 'PATCH', body: JSON.stringify(body) }), | |
| del: (path) => mutate(path, { method: 'DELETE' }), | |
| } | |
| // Multipart upload (files). Don't set Content-Type — the browser adds the boundary. | |
| export async function upload(path, formData) { | |
| const res = await fetch(path, { method: 'POST', body: formData }) | |
| if (!res.ok) { | |
| let detail = res.statusText | |
| try { detail = (await res.json()).detail || detail } catch { /* keep */ } | |
| throw new Error(detail) | |
| } | |
| return res.json() | |
| } | |
| // Poll a job until done/error. onProgress receives the list of messages. | |
| export async function pollJob(jobId, onProgress, intervalMs = 1200) { | |
| for (;;) { | |
| const job = await api.get(`/api/jobs/${jobId}`) | |
| if (onProgress) onProgress(job.progress || []) | |
| if (job.status === 'done') return job.result | |
| if (job.status === 'error') throw new Error(job.error || 'Job failed') | |
| await new Promise((r) => setTimeout(r, intervalMs)) | |
| } | |
| } | |
| // Convenience: POST that returns {job_id}, then poll it. | |
| export async function runJob(path, body, onProgress) { | |
| const { job_id } = await api.post(path, body) | |
| const result = await pollJob(job_id, onProgress) | |
| // The job writes state as it runs, so re-cache the bundle once it's done | |
| // (the POST above only saw pre-job state). | |
| const pid = projectIdFromPath(path) | |
| if (pid) syncProjectBundle(pid) | |
| return result | |
| } | |