12345678910111213import { listPairedDevices, markLastSyncedAt } from "./paired-devices";14import type { PairedDevice } from "../shared/sync-types";1516export type SyncTriggerReason = "vault_change" | "foreground" | "manual" | "mdns";1718export type SyncRunStatus = "idle" | "syncing" | "error";1920export interface SyncState {21 status: SyncRunStatus;22 reason?: SyncTriggerReason;23 lastSyncAt?: number;24 entriesReceived?: number;25 entriesSent?: number;26 error?: string;27}2829export interface SyncRunResult {30 entriesReceived: number;31 entriesSent: number;32}3334export type RunSyncWithDevice = (device: PairedDevice) => Promise<SyncRunResult>;3536let syncInFlight = false;37let lastState: SyncState = { status: "idle" };38let runner: RunSyncWithDevice | null = null;3940export function setSyncRunner(fn: RunSyncWithDevice | null): void {41 runner = fn;42}4344export function _resetSyncTriggerForTest(): void {45 syncInFlight = false;46 lastState = { status: "idle" };47 runner = null;48}4950export function getSyncState(): SyncState {51 return lastState;52}5354export async function triggerSyncIfReachable(opts: {55 reason: SyncTriggerReason;56 force?: boolean;57}): Promise<void> {58 59 60 61 if (syncInFlight && !opts.force) return;62 syncInFlight = true;6364 try {65 const devices = await listPairedDevices();66 const hosts = devices.filter(67 (d) => d.deviceType === "android" || d.deviceType === "ios",68 );69 if (hosts.length === 0) return;70 if (!runner) return;7172 setState({ status: "syncing", reason: opts.reason, error: undefined });73 await runSyncRounds(hosts);74 } finally {75 syncInFlight = false;76 }77}7879async function runSyncRounds(hosts: PairedDevice[]): Promise<void> {80 if (!runner) return;8182 let entriesReceived = 0;83 let entriesSent = 0;84 let anySucceeded = false;85 let firstError: string | undefined;8687 for (const host of hosts) {88 try {89 const result = await runner(host);90 entriesReceived += result.entriesReceived;91 entriesSent += result.entriesSent;92 await markLastSyncedAt(host.deviceId, Date.now());93 anySucceeded = true;94 } catch (err) {95 if (firstError === undefined) {96 firstError = (err as Error)?.message ?? String(err);97 }98 }99 }100101 if (anySucceeded) {102 setState({103 status: "idle",104 lastSyncAt: Date.now(),105 entriesReceived,106 entriesSent,107 error: undefined,108 });109 } else {110 setState({111 status: "error",112 error: firstError ?? "no_host_reachable",113 });114 }115}116117function setState(patch: Partial<SyncState>): void {118 lastState = { ...lastState, ...patch };119 broadcast(lastState);120}121122function broadcast(state: SyncState): void {123 if (typeof chrome === "undefined" || !chrome.runtime?.sendMessage) return;124 try {125 const maybeThenable = chrome.runtime.sendMessage({126 type: "SYNC_STATE_CHANGED",127 payload: state,128 });129 if (130 maybeThenable &&131 typeof (maybeThenable as Promise<unknown>).catch === "function"132 ) {133 (maybeThenable as Promise<unknown>).catch(() => {134 135 });136 }137 } catch {138 139 }140}