sync-alarm.ts
1// sync-alarm.ts — the 10-minute background sync tick.2//3// Four triggers, in order of reliability:4// 1. this alarm (survives MV3 service-worker death — only path that does)5// 2. SW activation hook in entry.ts (startup reason — first dial without6// waiting for the alarm)7// 3. device_list_changed on pairing/unpair (paired-devices.ts)8// 4. vault_change on every committed write (vault.ts fireSyncTrigger)9//10// All four call the same `triggerSyncIfReachable`. The in-flight guard there11// ensures a slow sync is never preempted by an alarm firing while it runs.1213import { triggerSyncIfReachable } from "./sync-trigger";1415/**16 * Single alarm name. Shared with the listener below — change together.17 * The `peach-` prefix keeps this distinct from any third-party alarm a18 * future extension may install.19 */20export const SYNC_ALARM_NAME = "peach-sync-periodic";2122/**23 * Chrome's recurring-alarm floor is 1 minute — anything shorter is treated24 * as a one-shot. 10 is well above that and the longest the user is likely25 * to wait for changes to propagate between paired devices without manual26 * action. Lowering this (e.g. for testing) must keep it >= 1.27 */28export const SYNC_ALARM_PERIOD_MINUTES = 10;2930let alarmListenerInstalled = false;3132/**33 * Schedule the recurring sync alarm. Idempotent — calling twice does not34 * stack two alarms. Uses `periodInMinutes` (Chrome's own repeat primitive)35 * rather than a manual reschedule loop, so a worker kill between fires does36 * not interrupt the schedule — Chrome holds the next-fire time.37 */38export function setupSyncAlarm(): void {39 if (!chrome.alarms?.create || !chrome.alarms?.clear) return;40 if (!alarmListenerInstalled) {41 // Static, not `await import()`: runtime import() throws in an MV3 service42 // worker (scripts/check-sw-static-imports.mjs pins this). triggerSyncIfReachable43 // is top-level-imported via sync-trigger, which itself statically imports44 // its deps — no cycle, no dynamic import.45 chrome.alarms.onAlarm.addListener((alarm) => {46 if (alarm.name !== SYNC_ALARM_NAME) return;47 void triggerSyncIfReachable({ reason: "alarm" }).catch(() => undefined);48 });49 alarmListenerInstalled = true;50 }51 // Clear-then-create so a changed period (config, future toggle) takes effect52 // without leaving a stale one-shot behind.53 chrome.alarms.clear(SYNC_ALARM_NAME);54 chrome.alarms.create(SYNC_ALARM_NAME, {55 periodInMinutes: SYNC_ALARM_PERIOD_MINUTES,56 });57}5859/**60 * Test seam. Resets the one-shot listener-install flag so a fresh61 * `setupSyncAlarm()` call re-registers against the (new) chrome mock.62 */63export function _resetSyncAlarmForTest(): void {64 alarmListenerInstalled = false;65}