123456789101112131415161718import { Capacitor, registerPlugin } from "@capacitor/core";19import type { PluginListenerHandle } from "@capacitor/core";20import type { DiscoveredPeer } from "./sync-types";2122export interface SyncBridgePeer {23 deviceId: string;24 deviceName: string;25 host: string;26 port: number;27 wsPort?: number;28 x25519PublicKey: string;29 pairing: boolean;30}3132export interface SyncBridge {33 34 startDiscovery(): Promise<void>;35 stopDiscovery(): Promise<void>;36 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void;37 onPeerLost(handler: (deviceId: string) => void): () => void;38}3940interface SyncPluginShape {41 startDiscovery(): Promise<{ alreadyDiscovering?: boolean } | void>;42 stopDiscovery(): Promise<void>;43 getDiscoveredPeers(): Promise<{ peers: SyncBridgePeer[] }>;44 addListener(45 eventName: "peerDiscovered",46 listener: (peer: SyncBridgePeer) => void47 ): Promise<PluginListenerHandle>;48 addListener(49 eventName: "peerLost",50 listener: (event: { deviceId: string }) => void51 ): Promise<PluginListenerHandle>;52 addListener(53 eventName: "syncError",54 listener: (event: { code: string; errorCode?: number }) => void55 ): Promise<PluginListenerHandle>;56}5758class CapacitorSyncBridge implements SyncBridge {59 private plugin = registerPlugin<SyncPluginShape>("Sync");60 private peerDiscoveredHandlers: Array<(peer: SyncBridgePeer) => void> = [];61 private peerLostHandlers: Array<(deviceId: string) => void> = [];62 private listenerHandles: PluginListenerHandle[] = [];6364 async startDiscovery(): Promise<void> {65 await this.plugin.startDiscovery();66 if (this.listenerHandles.length === 0) {67 const found = await this.plugin.addListener("peerDiscovered", (peer) => {68 for (const h of this.peerDiscoveredHandlers) h(peer);69 });70 const lost = await this.plugin.addListener("peerLost", (event) => {71 for (const h of this.peerLostHandlers) h(event.deviceId);72 });73 this.listenerHandles.push(found, lost);74 }75 }7677 async stopDiscovery(): Promise<void> {78 for (const handle of this.listenerHandles) {79 try {80 await handle.remove();81 } catch {82 83 }84 }85 this.listenerHandles = [];86 await this.plugin.stopDiscovery();87 }8889 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void {90 this.peerDiscoveredHandlers.push(handler);91 return () => {92 const i = this.peerDiscoveredHandlers.indexOf(handler);93 if (i >= 0) this.peerDiscoveredHandlers.splice(i, 1);94 };95 }9697 onPeerLost(handler: (deviceId: string) => void): () => void {98 this.peerLostHandlers.push(handler);99 return () => {100 const i = this.peerLostHandlers.indexOf(handler);101 if (i >= 0) this.peerLostHandlers.splice(i, 1);102 };103 }104}105106class NativeMessagingSyncBridge implements SyncBridge {107 private peerDiscoveredHandlers: Array<(peer: SyncBridgePeer) => void> = [];108 private peerLostHandlers: Array<(deviceId: string) => void> = [];109 private unsubscribers: Array<() => void> = [];110111 async startDiscovery(): Promise<void> {112 const sync = await import("../background/sync");113 sync.connectSyncHost();114 const offDiscovered = sync.onPeerDiscovered((peer) => {115 for (const h of this.peerDiscoveredHandlers) {116 h({117 deviceId: peer.deviceId,118 deviceName: peer.name,119 host: peer.host,120 port: peer.port,121 wsPort: peer.wsPort,122 x25519PublicKey: peer.x25519PublicKey,123 pairing: peer.pairing,124 });125 }126 });127 const offLost = sync.onPeerLost((deviceId) => {128 for (const h of this.peerLostHandlers) h(deviceId);129 });130 this.unsubscribers.push(offDiscovered, offLost);131 sync.discoverPeers();132 }133134 async stopDiscovery(): Promise<void> {135 for (const off of this.unsubscribers) off();136 this.unsubscribers = [];137 try {138 const sync = await import("../background/sync");139 if (sync.isHostConnected()) {140 await sync.stopDiscovery();141 }142 } catch {143 144 }145 }146147 onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void {148 this.peerDiscoveredHandlers.push(handler);149 return () => {150 const i = this.peerDiscoveredHandlers.indexOf(handler);151 if (i >= 0) this.peerDiscoveredHandlers.splice(i, 1);152 };153 }154155 onPeerLost(handler: (deviceId: string) => void): () => void {156 this.peerLostHandlers.push(handler);157 return () => {158 const i = this.peerLostHandlers.indexOf(handler);159 if (i >= 0) this.peerLostHandlers.splice(i, 1);160 };161 }162}163164let cached: SyncBridge | null = null;165166export function getSyncBridge(): SyncBridge {167 if (cached) return cached;168 cached = Capacitor.isNativePlatform()169 ? new CapacitorSyncBridge()170 : new NativeMessagingSyncBridge();171 return cached;172}173174175export function toDiscoveredPeer(176 peer: SyncBridgePeer,177 alreadyPaired: boolean178): DiscoveredPeer {179 return {180 deviceId: peer.deviceId,181 deviceName: peer.deviceName,182 host: peer.host,183 port: peer.port,184 wsPort: peer.wsPort,185 x25519PublicKey: peer.x25519PublicKey,186 alreadyPaired,187 };188}189190191192193194195export function peerWebSocketUrl(peer: SyncBridgePeer): string {196 if (peer.wsPort === undefined) {197 throw new Error("peer did not advertise a WebSocket port");198 }199 return `ws://${peer.host}:${peer.wsPort}/`;200}