Sync was push-on-demand only: a deck frozen on the laptop stayed in dirty
until someone opened Settings and pressed Sync now. The two stores whose
writes mean "the user just made something" now schedule one debounced
background syncNow("auto") through lib/sync-auto: debounced 2.5s so a burst
of writes is one push, one run at a time, silent on failure (the data stays
dirty and Settings still offers the manual sync), retried on the browser's
online event, and imported dynamically so the sync code stays out of the
first load with no import cycle.
markSyncDirty() now reports whether it actually marked, which keeps pull and
backup-import paths quiet while a real import pushes like any other local
change. Answers and chat keep the existing manual behaviour on purpose —
they change every few seconds while studying.
Tests: phase-1 (a deck write pushes itself, then Sync now is a no-op — 50
checks) and phase-3 (a check run pushes the new homework record — 30 checks).
README: decision 59. SW -> freezmo-v46.
63 lines
2.1 KiB
TypeScript
63 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Auto-push for people who already turned sync on: a deck or homework write
|
|
* schedules one background `syncNow("auto")` a moment later, so a deck frozen
|
|
* on the laptop shows up on the phone without opening Settings. Debounced (a
|
|
* burst of writes = one push), one run at a time, silent on failure — the data
|
|
* stays dirty and Settings still offers the manual sync.
|
|
*
|
|
* Deliberately scoped to decks and homework: answers and chat messages change
|
|
* constantly while studying, and pushing after every card would be noise.
|
|
* A push that fails offline is retried when the browser comes back online.
|
|
*
|
|
* Imported dynamically from the stores (lib/storage, lib/homework) so the
|
|
* sync module itself stays out of the first load and no import cycle forms.
|
|
*/
|
|
|
|
import { syncNow } from "./sync";
|
|
import { syncStore } from "./sync-state";
|
|
|
|
/** Long enough to swallow a burst (a save plus its follow-up writes). */
|
|
const DELAY_MS = 2500;
|
|
|
|
let timer: number | null = null;
|
|
let running = false;
|
|
let listening = false;
|
|
|
|
export function scheduleAutoSync(): void {
|
|
if (typeof window === "undefined") return;
|
|
const record = syncStore.getSnapshot();
|
|
if (!record.enabled || !record.id) return;
|
|
if (timer !== null) window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => {
|
|
timer = null;
|
|
void runAutoSync();
|
|
}, DELAY_MS);
|
|
listenForReconnect();
|
|
}
|
|
|
|
/** A push that failed while offline is retried when the connection returns. */
|
|
function listenForReconnect(): void {
|
|
if (listening) return;
|
|
listening = true;
|
|
window.addEventListener("online", () => {
|
|
const record = syncStore.getSnapshot();
|
|
if (record.enabled && record.dirtyAt !== null) scheduleAutoSync();
|
|
});
|
|
}
|
|
|
|
async function runAutoSync(): Promise<void> {
|
|
if (running) return;
|
|
if (typeof navigator !== "undefined" && navigator.onLine === false) return;
|
|
const record = syncStore.getSnapshot();
|
|
if (!record.enabled || record.dirtyAt === null) return;
|
|
running = true;
|
|
try {
|
|
await syncNow("auto");
|
|
} catch {
|
|
/* stays dirty on purpose: Settings still shows the change and syncs it */
|
|
} finally {
|
|
running = false;
|
|
}
|
|
}
|