// ── Canonical JSON ─────────────────────────────────────────── function sortKeys(obj: unknown): unknown { if (obj === null || obj === undefined || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map(sortKeys); const sorted: Record = {}; for (const key of Object.keys(obj as Record).sort()) { sorted[key] = sortKeys((obj as Record)[key]); } return sorted; } export function canonicalJson(obj: unknown): string { return JSON.stringify(sortKeys(obj)); } // ── Hashing ────────────────────────────────────────────────── export async function sha256Hex(data: string): Promise { const buf = await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(data), ); return Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } // ── Event filenames ────────────────────────────────────────── const HASH_PREFIX_LEN = 12; export function eventFilename(ts: number, fullHash: string): string { return `${ts}_${fullHash.slice(0, HASH_PREFIX_LEN)}.json`; } const EVENT_FILE_RE = /^(\d+)_([0-9a-f]+)\.json$/; export function parseEventFilename( name: string, ): { ts: number; hash: string } | null { const m = EVENT_FILE_RE.exec(name); if (!m) return null; return { ts: Number(m[1]), hash: m[2] }; } // ── Client ID ──────────────────────────────────────────────── export function generateClientId(): string { return crypto.randomUUID(); } // ── Index value extraction ─────────────────────────────────── export function extractIndexValue( data: unknown, fields: string[], ): IDBValidKey | undefined { if (!data || typeof data !== 'object') return undefined; const rec = data as Record; if (fields.length === 1) { const v = getNestedValue(rec, fields[0]); if (v === undefined || v === null) return undefined; return toIDBKey(v); } const parts: IDBValidKey[] = []; for (const f of fields) { const v = getNestedValue(rec, f); if (v === undefined || v === null) return undefined; parts.push(toIDBKey(v)); } return parts; } function getNestedValue(obj: Record, path: string): unknown { let cur: unknown = obj; for (const seg of path.split('.')) { if (cur === null || cur === undefined || typeof cur !== 'object') { return undefined; } cur = (cur as Record)[seg]; } return cur; } function toIDBKey(v: unknown): IDBValidKey { if (typeof v === 'string' || typeof v === 'number') return v; if (v instanceof Date) return v; if (Array.isArray(v)) return v.map(toIDBKey); // Fallback: stringify return String(v); }