manifest.ts

2.5 KB · raw

1import { VaultEntry } from "../shared/types";23export interface ManifestEntry {4  id: string;5  modifiedAt: number;6  deletedAt?: number;7  hash: string;8}910export interface ManifestDiffResult {11  sendToPeer: ManifestEntry[];12  requestFromPeer: string[];13  conflicts: ManifestEntry[];14}1516const GC_TTL_MS = 90 * 24 * 60 * 60 * 1000;1718export function diffManifests(19  localManifest: ManifestEntry[],20  remoteManifest: ManifestEntry[]21): ManifestDiffResult {22  const localMap = new Map<string, ManifestEntry>(localManifest.map((e) => [e.id, e]));23  const remoteMap = new Map<string, ManifestEntry>(remoteManifest.map((e) => [e.id, e]));2425  const sendToPeer: ManifestEntry[] = [];26  const requestFromPeer: string[] = [];27  const conflicts: ManifestEntry[] = [];2829  const allIds = new Set([...localMap.keys(), ...remoteMap.keys()]);3031  for (const id of allIds) {32    const local = localMap.get(id);33    const remote = remoteMap.get(id);3435    if (local && !remote) {36      sendToPeer.push(local);37    } else if (!local && remote) {38      requestFromPeer.push(id);39    } else if (local && remote) {40      if (local.modifiedAt === remote.modifiedAt) {41        if (local.hash !== remote.hash) {42          conflicts.push(resolveConflict(local, remote));43        }44      } else if (local.modifiedAt > remote.modifiedAt) {45        sendToPeer.push(local);46      } else {47        requestFromPeer.push(id);48      }49    }50  }5152  return { sendToPeer, requestFromPeer, conflicts };53}5455export function resolveConflict(a: ManifestEntry, b: ManifestEntry): ManifestEntry {56  return a.id >= b.id ? a : b;57}5859export function gcTombstones(entries: ManifestEntry[], nowMs: number): ManifestEntry[] {60  return entries.filter(61    (e) => !(e.deletedAt && e.deletedAt > 0 && nowMs - e.deletedAt > GC_TTL_MS)62  );63}6465export async function contentHash(entry: VaultEntry): Promise<string> {66  const { id: _id, modifiedAt: _modifiedAt, deletedAt: _deletedAt, ...rest } = entry;67  const json = JSON.stringify(rest);68  const encoded = new TextEncoder().encode(json);69  const digest = await crypto.subtle.digest("SHA-256", encoded);70  const bytes = new Uint8Array(digest).slice(0, 16);71  return Array.from(bytes)72    .map((b) => b.toString(16).padStart(2, "0"))73    .join("");74}7576export async function buildManifest(entries: VaultEntry[]): Promise<ManifestEntry[]> {77  return Promise.all(78    entries.map(async (entry) => ({79      id: entry.id,80      modifiedAt: entry.modifiedAt,81      deletedAt: entry.deletedAt,82      hash: await contentHash(entry),83    }))84  );85}