import { useP2PSettingsPreparation } from "@/serviceFeatures/useP2PSettingsPreparation"; import { type P2PSyncSetting, SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; import type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon"; import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector"; import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature"; import { ServiceContext, type LiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context"; import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p"; import type { ValueStore, ValueDeleter } from "@vrtmrz/value-store"; import { createWebStorageValueStore } from "@vrtmrz/value-store/web-storage"; import { unique } from "octagonal-wheels/collection"; import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; import { createLiveSyncBrowserServiceHub, type LiveSyncBrowserServiceHub, } from "@/apps/browser/createLiveSyncBrowserServiceHub"; import { Menu } from "@/apps/browser/BrowserMenu"; import type { P2PReplicatorPaneHost } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneHost"; import { translateLiveSyncMessage } from "@/common/translation"; import { WEBPEER_STORE_NAME, createWebPeerPersistence } from "./WebPeerPersistence"; export interface WebPeerRuntimeOptions { deviceValues?: ValueStore & ValueDeleter; context?: ServiceContext; store?: SimpleStore; deviceName?: string; systemVaultName?: string; } function addToList(item: string, list: string): string { return unique( list .split(",") .map((entry) => entry.trim()) .concat(item) .filter(Boolean) ).join(","); } function removeFromList(item: string, list: string): string { return list .split(",") .map((entry) => entry.trim()) .filter((entry) => entry !== item) .filter(Boolean) .join(","); } export class WebPeerRuntime { readonly context: ServiceContext; readonly services: LiveSyncBrowserServiceHub; readonly p2p: P2PServiceViews; readonly p2pLogCollector: P2PLogCollector; readonly paneHost: P2PReplicatorPaneHost; private menu?: Menu; private restartScheduled = false; private startPromise?: Promise; private shutdownPromise?: Promise; constructor(private readonly options: WebPeerRuntimeOptions = {}) { const persistence = createWebPeerPersistence(options.store); this.context = options.context ?? new ServiceContext({ translate: translateLiveSyncMessage }); this.services = createLiveSyncBrowserServiceHub({ context: this.context, deviceValues: options.deviceValues ?? createWebStorageValueStore(compatGlobal.localStorage, { prefix: "" }), getSystemVaultName: () => options.systemVaultName ?? WEBPEER_STORE_NAME, settings: persistence.settings, restart: { schedule: () => this.scheduleRestart(), perform: () => this.scheduleRestart(), ask: () => this.scheduleRestart(), isScheduled: () => this.restartScheduled, }, }); this.p2p = useP2PReplicatorFeature({ services: this.services, serviceModules: {} }, undefined, undefined, { prepareP2PSettings: useP2PSettingsPreparation(this.services.API.webCompatFetch.bind(this.services.API)), }); this.p2pLogCollector = new P2PLogCollector(this.events); this.paneHost = { services: this.services, p2p: this.p2p, showPeerMenu: (peer, event) => this.showPeerMenu(peer, event), }; } get events(): LiveSyncEventHub { return this.context.events; } get settings(): P2PSyncSetting { return this.services.setting.currentSettings(); } get statusLine() { return this.p2pLogCollector.p2pReplicationLine; } start(): Promise { this.startPromise ??= this.startRuntime(); return this.startPromise; } private async startRuntime(): Promise { await this.services.setting.loadSettings(); const deviceName = this.options.deviceName?.trim(); if (deviceName) { await this.services.config.setValue(SETTING_KEY_P2P_DEVICE_NAME, deviceName); } const opened = await this.services.database.openDatabase({ replicator: this.services.replicator, databaseEvents: this.services.databaseEvents, }); if (!opened) { throw new Error("WebPeer local database could not be opened"); } this.services.appLifecycle.markIsReady(); this.events.emitEvent(EVENT_LAYOUT_READY); await this.services.appLifecycle.onResumed(); return this; } shutdown(): Promise { this.shutdownPromise ??= this.shutdownRuntime(); return this.shutdownPromise; } private async shutdownRuntime(): Promise { this.menu?.hide(); this.menu = undefined; if (!this.services.control.hasUnloaded()) { await this.services.control.onUnload(); } } private scheduleRestart(): void { if (this.restartScheduled) { return; } this.restartScheduled = true; compatGlobal.setTimeout(() => compatGlobal.location.reload(), 0); } private showPeerMenu(peer: PeerStatus, event: MouseEvent): void { this.menu?.hide(); this.menu = new Menu() .addItem((item) => item.setTitle("📥 Only fetch").onClick(async () => { await this.p2p.targetedTransfer.pullFromPeer(peer.peerId); }) ) .addItem((item) => item.setTitle("📤 Only send").onClick(async () => { await this.p2p.targetedTransfer.requestPushToPeer(peer.peerId); }) ) .addSeparator() .addItem((item) => { item.setTitle("Toggle sync on connect") .onClick(() => this.togglePeerSetting(peer, "syncOnConnect")) .setIcon(peer.syncOnConnect ? "checkmark" : null); }) .addItem((item) => { item.setTitle("Toggle watch on connect") .onClick(() => this.togglePeerSetting(peer, "watchOnConnect")) .setIcon(peer.watchOnConnect ? "checkmark" : null); }) .addItem((item) => { item.setTitle("Toggle sync on `Replicate now` command") .onClick(() => this.togglePeerSetting(peer, "syncOnReplicationCommand")) .setIcon(peer.syncOnReplicationCommand ? "checkmark" : null); }); void this.menu.showAtPosition({ x: event.x, y: event.y }); } private async togglePeerSetting( peer: PeerStatus, property: "syncOnConnect" | "watchOnConnect" | "syncOnReplicationCommand" ): Promise { const settingMap = { syncOnConnect: "P2P_AutoSyncPeers", watchOnConnect: "P2P_AutoWatchPeers", syncOnReplicationCommand: "P2P_SyncOnReplication", } as const; const settingKey = settingMap[property]; const currentValue = this.services.setting.currentSettings()[settingKey] ?? ""; await this.services.setting.applyPartial( { [settingKey]: peer[property] ? removeFromList(peer.name, currentValue) : addToList(peer.name, currentValue), }, true ); } async getDeviceName(): Promise { return (await this.services.config.getValue(SETTING_KEY_P2P_DEVICE_NAME)) || this.services.vault.getVaultName(); } }