1import type { SyncTransport } from '../shared/websocket-sync-transport';2import type { SyncableDevice } from '../shared/sync-types';3import { encodeSyncMessage, decodeSyncMessage } from '../shared/sync-wire';45export interface SyncDeps {6 getVaultBlobForSync: () => Promise<{7 blob: string;8 timestamp: number;9 license?: string;10 devices: SyncableDevice[];11 } | null>;12 applyIncomingVault: (13 blob: string,14 timestamp: number,15 license?: string,16 devices?: SyncableDevice[],17 ) => Promise<boolean>;18 isVaultUnlocked: () => boolean;19}2021export interface SyncResult {22 status: 'completed' | 'skipped' | 'errored';23 applied: boolean;24 error?: string;25}2627282930export async function runBlobSyncSession(31 role: 'initiator' | 'responder',32 transport: SyncTransport,33 deps: SyncDeps,34): Promise<SyncResult> {35 if (!deps.isVaultUnlocked()) {36 return { status: 'skipped', applied: false, error: 'vault_locked' };37 }3839 try {40 if (role === 'initiator') {41 await transport.send(encodeSyncMessage({ type: 'SYNC_HELLO' }));4243 44 const remoteMsg = decodeSyncMessage(await transport.receive());45 let applied = false;4647 if (remoteMsg.type === 'SYNC_VAULT_SEND') {48 applied = await deps.applyIncomingVault(49 remoteMsg.blob,50 remoteMsg.timestamp,51 remoteMsg.license,52 remoteMsg.devices,53 );54 await transport.send(encodeSyncMessage({ type: 'SYNC_VAULT_ACK', applied }));55 } else if (remoteMsg.type !== 'SYNC_DONE') {56 throw new Error(`unexpected message: ${remoteMsg.type}`);57 }5859 60 const local = await deps.getVaultBlobForSync();61 if (local) {62 await transport.send(encodeSyncMessage({63 type: 'SYNC_VAULT_SEND',64 blob: local.blob,65 timestamp: local.timestamp,66 license: local.license,67 devices: local.devices,68 }));69 const ack = decodeSyncMessage(await transport.receive());70 if (ack.type !== 'SYNC_VAULT_ACK') throw new Error(`expected ACK, got ${ack.type}`);71 }7273 await transport.send(encodeSyncMessage({ type: 'SYNC_DONE' }));74 await transport.receive(); 75 await transport.close();76 return { status: 'completed', applied };7778 } else {79 80 const hello = decodeSyncMessage(await transport.receive());81 if (hello.type !== 'SYNC_HELLO') throw new Error(`expected SYNC_HELLO, got ${hello.type}`);8283 84 const local = await deps.getVaultBlobForSync();85 if (local) {86 await transport.send(encodeSyncMessage({87 type: 'SYNC_VAULT_SEND',88 blob: local.blob,89 timestamp: local.timestamp,90 license: local.license,91 devices: local.devices,92 }));93 const ack = decodeSyncMessage(await transport.receive());94 if (ack.type !== 'SYNC_VAULT_ACK') throw new Error(`expected ACK, got ${ack.type}`);95 } else {96 await transport.send(encodeSyncMessage({ type: 'SYNC_DONE' }));97 }9899 100 const remoteMsg = decodeSyncMessage(await transport.receive());101 let applied = false;102 if (remoteMsg.type === 'SYNC_VAULT_SEND') {103 applied = await deps.applyIncomingVault(104 remoteMsg.blob,105 remoteMsg.timestamp,106 remoteMsg.license,107 remoteMsg.devices,108 );109 await transport.send(encodeSyncMessage({ type: 'SYNC_VAULT_ACK', applied }));110 }111112 113 const done = decodeSyncMessage(await transport.receive());114 if (done.type === 'SYNC_DONE') {115 await transport.send(encodeSyncMessage({ type: 'SYNC_DONE' }));116 }117118 await transport.close();119 return { status: 'completed', applied };120 }121 } catch (err) {122 await transport.close().catch(() => {});123 return { status: 'errored', applied: false, error: String(err) };124 }125}