import { getSignatures, getKeysInWindow, getMessageByHash, } from '../utils/relay'; import type { RelayConfig } from '../types/identity'; import type { LocalIdentity } from '../types/identity'; import type { MsgChiffre, MsgSignature, MsgCle } from '../types/message'; import { GraphResolver } from './graphResolver'; import { validateDecryptedMessage } from './syncValidate'; import { updateGraphFromMessage } from './syncUpdateGraph'; import { tryDecryptWithKeys } from './syncDecrypt'; import { HashCache } from '../utils/cache'; import { runSyncLoop, type SyncOneRelayResult } from './syncLoop'; /** * Service for synchronizing messages from relays. * Scan-first flow: fetch keys in window → fetch messages by hash → ECDH decrypt. */ export class SyncService { private readonly relays: RelayConfig[]; private readonly graphResolver: GraphResolver; private readonly hashCache: HashCache; private readonly identity: LocalIdentity | null; constructor( relays: RelayConfig[], graphResolver: GraphResolver, identity?: LocalIdentity | null, ) { this.relays = relays; this.graphResolver = graphResolver; this.hashCache = new HashCache(); this.identity = identity ?? null; } /** * Initialize HashCache (load from IndexedDB). Call before sync(). */ async init(): Promise { await this.hashCache.init(); } /** * Scan-first sync for one relay: fetch keys in window, group by hash, * fetch messages by hash, ECDH decrypt, validate, update graph. */ private async syncOneRelay( endpoint: string, start: number, end: number, _serviceUuid?: string, ): Promise { try { const keys = await getKeysInWindow(endpoint, start, end); const byHash = new Map(); for (const k of keys) { const h = k.hash_message; const list = byHash.get(h) ?? []; list.push(k); byHash.set(h, list); } const newHashes: string[] = []; let messages = 0; let decrypted = 0; let validated = 0; let indechiffrable = 0; let nonValide = 0; for (const [hash, keyList] of byHash) { if (this.hashCache.hasSeen(hash)) { continue; } newHashes.push(hash); this.hashCache.markSeen(hash); let msg: MsgChiffre; try { msg = await getMessageByHash(endpoint, hash); } catch { indechiffrable++; continue; } messages++; if (this.identity === null) { indechiffrable++; continue; } const dec = await tryDecryptWithKeys(msg, keyList, this.identity); if (dec === null) { indechiffrable++; continue; } decrypted++; const valid = await validateDecryptedMessage( dec, (h) => this.fetchSignatures(h), ); if (valid) { updateGraphFromMessage(dec, this.graphResolver); validated++; } else { nonValide++; } } return { ok: true, newHashes, messages, newMessages: newHashes.length, decrypted, validated, indechiffrable, nonValide, }; } catch (error) { console.error(`Error syncing from ${endpoint}:`, error); return this.emptyRelayResult(); } } private emptyRelayResult(): SyncOneRelayResult { return { ok: false, newHashes: [], messages: 0, newMessages: 0, decrypted: 0, validated: 0, indechiffrable: 0, nonValide: 0, }; } /** * Synchronize messages from all enabled relays. * Uses scan-first flow (keys in window → fetch by hash → ECDH decrypt). */ async sync( start: number, end: number, serviceUuid?: string, ): Promise<{ messages: number; newMessages: number; decrypted: number; validated: number; indechiffrable: number; nonValide: number; relayStatus: Array<{ endpoint: string; ok: boolean }>; }> { const fetchOne = ( ep: string, s: number, e: number, u?: string, ): Promise => this.syncOneRelay(ep, s, e, u); const { acc, relayStatus, allNewHashes } = await runSyncLoop({ relays: this.relays, fetchOne, start, end, serviceUuid, }); if (allNewHashes.length > 0) { await this.hashCache.markSeenBatch(allNewHashes); } return { ...acc, relayStatus }; } /** * Fetch signatures for a message hash (all enabled relays). */ async fetchSignatures(hash: string): Promise { const allSignatures: MsgSignature[] = []; for (const relay of this.relays) { if (!relay.enabled) { continue; } try { const sigs = await getSignatures(relay.endpoint, hash); allSignatures.push(...sigs); } catch (error) { console.error(`Error fetching signatures from ${relay.endpoint}:`, error); } } return allSignatures; } }