123456789101112import { registerPlugin } from "@capacitor/core";13import type { PluginListenerHandle } from "@capacitor/core";14import type { DiscoveredPeer } from "./sync-types";15import type { SyncSessionResult } from "../background/sync-coordinator";1617export interface SyncBridgePeer {18 deviceId: string;19 deviceName: string;20 host: string;21 port: number;22 wsPort?: number;23 x25519PublicKey: string;24 pairing: boolean;25}2627export interface SyncBridge {28 29 startDiscovery(): Promise<void>;30 stopDiscovery(): Promise<void>;31 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void;32 onPeerLost(handler: (deviceId: string) => void): () => void;33 343536373839 syncWithPeer?(peer: DiscoveredPeer): Promise<SyncSessionResult>;40}4142interface SyncPluginShape {43 startDiscovery(): Promise<{ alreadyDiscovering?: boolean } | void>;44 stopDiscovery(): Promise<void>;45 getDiscoveredPeers(): Promise<{ peers: SyncBridgePeer[] }>;46 addListener(47 eventName: "peerDiscovered",48 listener: (peer: SyncBridgePeer) => void49 ): Promise<PluginListenerHandle>;50 addListener(51 eventName: "peerLost",52 listener: (event: { deviceId: string }) => void53 ): Promise<PluginListenerHandle>;54 addListener(55 eventName: "syncError",56 listener: (event: { code: string; errorCode?: number }) => void57 ): Promise<PluginListenerHandle>;58}5960class CapacitorSyncBridge implements SyncBridge {61 private plugin = registerPlugin<SyncPluginShape>("Sync");62 private peerDiscoveredHandlers: Array<(peer: SyncBridgePeer) => void> = [];63 private peerLostHandlers: Array<(deviceId: string) => void> = [];64 private listenerHandles: PluginListenerHandle[] = [];6566 async startDiscovery(): Promise<void> {67 await this.plugin.startDiscovery();68 if (this.listenerHandles.length === 0) {69 const found = await this.plugin.addListener("peerDiscovered", (peer) => {70 for (const h of this.peerDiscoveredHandlers) h(peer);71 });72 const lost = await this.plugin.addListener("peerLost", (event) => {73 for (const h of this.peerLostHandlers) h(event.deviceId);74 });75 this.listenerHandles.push(found, lost);76 }77 }7879 async stopDiscovery(): Promise<void> {80 for (const handle of this.listenerHandles) {81 try {82 await handle.remove();83 } catch {84 85 }86 }87 this.listenerHandles = [];88 await this.plugin.stopDiscovery();89 }9091 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void {92 this.peerDiscoveredHandlers.push(handler);93 return () => {94 const i = this.peerDiscoveredHandlers.indexOf(handler);95 if (i >= 0) this.peerDiscoveredHandlers.splice(i, 1);96 };97 }9899 onPeerLost(handler: (deviceId: string) => void): () => void {100 this.peerLostHandlers.push(handler);101 return () => {102 const i = this.peerLostHandlers.indexOf(handler);103 if (i >= 0) this.peerLostHandlers.splice(i, 1);104 };105 }106107 async syncWithPeer(peer: DiscoveredPeer): Promise<SyncSessionResult> {108 const [{ getAndroidSyncIdentity }, { createCapacitorSyncTransport }, { runBlobSyncSession }, { storageGet }] =109 await Promise.all([110 import("./android-sync-identity"),111 import("./capacitor-sync-transport"),112 import("../background/sync-coordinator"),113 import("./storage"),114 ]);115116 const identity = await getAndroidSyncIdentity();117 const transport = await createCapacitorSyncTransport(118 peer,119 identity.privateKey,120 identity.publicKey,121 );122123 return runBlobSyncSession("initiator", transport, {124 125 126 127 128 isPro: async () => true,129 isUnlocked: () => true,130 getVaultBlobs: async () => ({131 vault_a: (await storageGet("peach_vault_a")) ?? "",132 vault_b: (await storageGet("peach_vault_b")) ?? "",133 }),134 });135 }136}137138let cached: SyncBridge | null = null;139140export function getSyncBridge(): SyncBridge {141 if (cached) return cached;142 cached = new CapacitorSyncBridge();143 return cached;144}145146147export function toDiscoveredPeer(148 peer: SyncBridgePeer,149 alreadyPaired: boolean150): DiscoveredPeer {151 return {152 deviceId: peer.deviceId,153 deviceName: peer.deviceName,154 host: peer.host,155 port: peer.port,156 wsPort: peer.wsPort,157 x25519PublicKey: peer.x25519PublicKey,158 alreadyPaired,159 };160}161162163164165166167export function peerWebSocketUrl(peer: SyncBridgePeer): string {168 if (peer.wsPort === undefined) {169 throw new Error("peer did not advertise a WebSocket port");170 }171 return `ws://${peer.host}:${peer.wsPort}/`;172}