import type { SyncTransport } from '../shared/websocket-sync-transport'; import type { SyncableDevice } from '../shared/sync-types'; import { encodeSyncMessage, decodeSyncMessage } from '../shared/sync-wire'; export interface SyncDeps { getVaultBlobForSync: () => Promise<{ blob: string; timestamp: number; license?: string; devices: SyncableDevice[]; } | null>; applyIncomingVault: ( blob: string, timestamp: number, license?: string, devices?: SyncableDevice[], ) => Promise; isVaultUnlocked: () => boolean; } export interface SyncResult { status: 'completed' | 'skipped' | 'errored'; applied: boolean; error?: string; } // Full bidirectional vault sync session — last-write-wins on timestamp. // Initiator sends SYNC_HELLO, responder sends its vault first, then initiator // sends its vault. Both sides apply if the remote is newer. export async function runBlobSyncSession( role: 'initiator' | 'responder', transport: SyncTransport, deps: SyncDeps, ): Promise { if (!deps.isVaultUnlocked()) { return { status: 'skipped', applied: false, error: 'vault_locked' }; } try { if (role === 'initiator') { await transport.send(encodeSyncMessage({ type: 'SYNC_HELLO' })); // Receive remote vault first const remoteMsg = decodeSyncMessage(await transport.receive()); let applied = false; if (remoteMsg.type === 'SYNC_VAULT_SEND') { applied = await deps.applyIncomingVault( remoteMsg.blob, remoteMsg.timestamp, remoteMsg.license, remoteMsg.devices, ); await transport.send(encodeSyncMessage({ type: 'SYNC_VAULT_ACK', applied })); } else if (remoteMsg.type !== 'SYNC_DONE') { throw new Error(`unexpected message: ${remoteMsg.type}`); } // Send our vault const local = await deps.getVaultBlobForSync(); if (local) { await transport.send(encodeSyncMessage({ type: 'SYNC_VAULT_SEND', blob: local.blob, timestamp: local.timestamp, license: local.license, devices: local.devices, })); const ack = decodeSyncMessage(await transport.receive()); if (ack.type !== 'SYNC_VAULT_ACK') throw new Error(`expected ACK, got ${ack.type}`); } await transport.send(encodeSyncMessage({ type: 'SYNC_DONE' })); await transport.receive(); // wait for remote SYNC_DONE await transport.close(); return { status: 'completed', applied }; } else { // Responder: wait for SYNC_HELLO const hello = decodeSyncMessage(await transport.receive()); if (hello.type !== 'SYNC_HELLO') throw new Error(`expected SYNC_HELLO, got ${hello.type}`); // Send our vault first const local = await deps.getVaultBlobForSync(); if (local) { await transport.send(encodeSyncMessage({ type: 'SYNC_VAULT_SEND', blob: local.blob, timestamp: local.timestamp, license: local.license, devices: local.devices, })); const ack = decodeSyncMessage(await transport.receive()); if (ack.type !== 'SYNC_VAULT_ACK') throw new Error(`expected ACK, got ${ack.type}`); } else { await transport.send(encodeSyncMessage({ type: 'SYNC_DONE' })); } // Receive initiator's vault const remoteMsg = decodeSyncMessage(await transport.receive()); let applied = false; if (remoteMsg.type === 'SYNC_VAULT_SEND') { applied = await deps.applyIncomingVault( remoteMsg.blob, remoteMsg.timestamp, remoteMsg.license, remoteMsg.devices, ); await transport.send(encodeSyncMessage({ type: 'SYNC_VAULT_ACK', applied })); } // Wait for SYNC_DONE then echo it const done = decodeSyncMessage(await transport.receive()); if (done.type === 'SYNC_DONE') { await transport.send(encodeSyncMessage({ type: 'SYNC_DONE' })); } await transport.close(); return { status: 'completed', applied }; } } catch (err) { await transport.close().catch(() => {}); return { status: 'errored', applied: false, error: String(err) }; } }