import type { TimeRecord } from "../shared/record.ts"; import { allRecords, applyRemote, cursor, pendingOperations, removeOperations } from "./store.ts"; export type SyncStatus = "offline" | "syncing" | "synced" | "pending" | "signed-out"; let current: Promise | null = null; let rerunRequested = false; class SignedOut extends Error {} async function api(url: string, init?: RequestInit): Promise { const response = await fetch(url, init); if (response.status === 401) throw new SignedOut(); if (!response.ok) throw new Error(`Sync failed (${response.status})`); return response.json() as Promise; } export function syncNow(onChange: (status: SyncStatus, records?: TimeRecord[]) => void): Promise { if (current) { rerunRequested = true; return current; } current = (async () => { if (!navigator.onLine) { onChange("offline"); return; } onChange("syncing"); try { let pending = await pendingOperations(); while (pending.length) { const batch = pending.slice(0, 200); const body = await api<{ accepted: string[] }>("/api/operations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ operations: batch.map(({ id, record }) => ({ id, record })) }), }); await removeOperations(body.accepted); pending = await pendingOperations(); } let since = await cursor(); for (;;) { const body = await api<{ records: TimeRecord[]; cursor: number; hasMore: boolean }>( `/api/state?since=${since}`, { cache: "no-store" }); await applyRemote(body.records, body.cursor); since = body.cursor; if (!body.hasMore) break; } onChange("synced", await allRecords()); } catch (error) { if (error instanceof SignedOut) { onChange("signed-out"); return; } console.error(error); onChange("pending"); } })().finally(() => { current = null; if (rerunRequested) { rerunRequested = false; void syncNow(onChange); } }); return current; }