12345678910import { buildSyncManifest, diffSyncManifests } from "../shared/sync-protocol";11import type { SyncDiff } from "../shared/sync-protocol";12import type { SyncManifestEntry } from "../shared/sync-types";13import { encodeSyncMessage } from "../shared/sync-wire";14import type { VaultEntry } from "../shared/types";1516export type SyncPreflightError = "pro_required" | "vault_locked";1718export interface SyncPreflightResult {19 ok: boolean;20 error?: SyncPreflightError;21}2223export interface SyncSnapshot {24 entries: VaultEntry[];25 manifest: SyncManifestEntry[];26}2728export interface SyncPlan {29 localManifest: SyncManifestEntry[];30 diff: SyncDiff;31}3233export interface SyncCoordinatorDeps {34 isProUser: () => Promise<boolean>;35 isUnlocked: () => boolean;36 getEntries: () => VaultEntry[];37 sendSyncData: (connectionId: string, data: string) => void;38}3940async function loadDefaultDeps(): Promise<SyncCoordinatorDeps> {41 const [license, sync, vault] = await Promise.all([42 import("./license"),43 import("./sync"),44 import("./vault"),45 ]);46 return {47 isProUser: license.isProUser,48 isUnlocked: vault.isUnlocked,49 getEntries: vault.getEntries,50 sendSyncData: sync.sendSyncData,51 };52}5354export async function checkSyncPreflight(55 deps?: Pick<SyncCoordinatorDeps, "isProUser" | "isUnlocked">56): Promise<SyncPreflightResult> {57 const readyDeps = deps ?? (await loadDefaultDeps());58 if (!(await readyDeps.isProUser())) return { ok: false, error: "pro_required" };59 if (!readyDeps.isUnlocked()) return { ok: false, error: "vault_locked" };60 return { ok: true };61}6263export async function buildLocalSyncSnapshot(64 deps?: Pick<SyncCoordinatorDeps, "isProUser" | "isUnlocked" | "getEntries">65): Promise<SyncSnapshot> {66 const readyDeps = deps ?? (await loadDefaultDeps());67 const preflight = await checkSyncPreflight(readyDeps);68 if (!preflight.ok) throw new Error(preflight.error);6970 const entries = readyDeps.getEntries();71 return {72 entries,73 manifest: await buildSyncManifest(entries),74 };75}7677export async function planManifestSync(78 remoteManifest: SyncManifestEntry[],79 deps?: Pick<SyncCoordinatorDeps, "isProUser" | "isUnlocked" | "getEntries">80): Promise<SyncPlan> {81 const snapshot = await buildLocalSyncSnapshot(deps);82 return {83 localManifest: snapshot.manifest,84 diff: diffSyncManifests(snapshot.manifest, remoteManifest),85 };86}8788export async function cancelSyncIfVaultLocked(89 connectionId: string,90 deps?: Pick<SyncCoordinatorDeps, "isUnlocked" | "sendSyncData">91): Promise<boolean> {92 const readyDeps = deps ?? (await loadDefaultDeps());93 if (readyDeps.isUnlocked()) return false;9495 readyDeps.sendSyncData(96 connectionId,97 encodeSyncMessage({98 type: "SYNC_ERROR",99 code: "vault_locked",100 message: "Vault is locked; unlock Peach before syncing.",101 })102 );103 return true;104}