58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
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<void> | null = null;
|
||
|
|
let rerunRequested = false;
|
||
|
|
|
||
|
|
class SignedOut extends Error {}
|
||
|
|
|
||
|
|
async function api<T>(url: string, init?: RequestInit): Promise<T> {
|
||
|
|
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<T>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function syncNow(onChange: (status: SyncStatus, records?: TimeRecord[]) => void): Promise<void> {
|
||
|
|
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;
|
||
|
|
}
|