import { VaultEntry } from "../shared/types"; export interface ManifestEntry { id: string; modifiedAt: number; deletedAt?: number; hash: string; } export interface ManifestDiffResult { sendToPeer: ManifestEntry[]; requestFromPeer: string[]; conflicts: ManifestEntry[]; } const GC_TTL_MS = 90 * 24 * 60 * 60 * 1000; export function diffManifests( localManifest: ManifestEntry[], remoteManifest: ManifestEntry[] ): ManifestDiffResult { const localMap = new Map(localManifest.map((e) => [e.id, e])); const remoteMap = new Map(remoteManifest.map((e) => [e.id, e])); const sendToPeer: ManifestEntry[] = []; const requestFromPeer: string[] = []; const conflicts: ManifestEntry[] = []; const allIds = new Set([...localMap.keys(), ...remoteMap.keys()]); for (const id of allIds) { const local = localMap.get(id); const remote = remoteMap.get(id); if (local && !remote) { sendToPeer.push(local); } else if (!local && remote) { requestFromPeer.push(id); } else if (local && remote) { if (local.modifiedAt === remote.modifiedAt) { if (local.hash !== remote.hash) { conflicts.push(resolveConflict(local, remote)); } } else if (local.modifiedAt > remote.modifiedAt) { sendToPeer.push(local); } else { requestFromPeer.push(id); } } } return { sendToPeer, requestFromPeer, conflicts }; } export function resolveConflict(a: ManifestEntry, b: ManifestEntry): ManifestEntry { return a.id >= b.id ? a : b; } export function gcTombstones(entries: ManifestEntry[], nowMs: number): ManifestEntry[] { return entries.filter( (e) => !(e.deletedAt && e.deletedAt > 0 && nowMs - e.deletedAt > GC_TTL_MS) ); } export async function contentHash(entry: VaultEntry): Promise { const { id: _id, modifiedAt: _modifiedAt, deletedAt: _deletedAt, ...rest } = entry; const json = JSON.stringify(rest); const encoded = new TextEncoder().encode(json); const digest = await crypto.subtle.digest("SHA-256", encoded); const bytes = new Uint8Array(digest).slice(0, 16); return Array.from(bytes) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } export async function buildManifest(entries: VaultEntry[]): Promise { return Promise.all( entries.map(async (entry) => ({ id: entry.id, modifiedAt: entry.modifiedAt, deletedAt: entry.deletedAt, hash: await contentHash(entry), })) ); }