From d5e2f57781d0ed7e91cf89b11a57db430c426ac8 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 15 May 2026 10:18:53 +0100 Subject: [PATCH 1/7] Fixed: fixed P2P bugs and and implement new UI --- docs/p2p_sync_updates_2026.md | 42 ++ manifest.json | 18 +- .../P2PReplicator/P2POpenReplicationModal.ts | 69 +++ .../P2POpenReplicationPane.svelte | 288 +++++++++ .../P2PSync/P2PReplicator/P2PReplicationUI.ts | 73 +++ .../P2PReplicator/P2PServerStatusCard.svelte | 201 +++++++ .../P2PReplicator/P2PServerStatusPane.svelte | 558 ++++++++++++++++++ .../P2PReplicator/P2PServerStatusPaneView.ts | 42 ++ src/lib | 2 +- src/main.ts | 8 +- src/modules/core/ModuleReplicator.ts | 1 + .../SetupWizard/dialogs/SetupRemoteP2P.svelte | 22 +- src/modules/services/ObsidianAPIService.ts | 14 + src/serviceFeatures/useP2PReplicatorUI.ts | 69 ++- updates.md | 12 + 15 files changed, 1388 insertions(+), 31 deletions(-) create mode 100644 docs/p2p_sync_updates_2026.md create mode 100644 src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts create mode 100644 src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte create mode 100644 src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts create mode 100644 src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte create mode 100644 src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte create mode 100644 src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md new file mode 100644 index 0000000..5a8f9e4 --- /dev/null +++ b/docs/p2p_sync_updates_2026.md @@ -0,0 +1,42 @@ +# User Guide: Peer-to-Peer Synchronisation (2026 Edition) + +Peer-to-Peer (P2P) synchronisation has evolved significantly. This guide covers the essential setup and the new features introduced in the 2026 updates. + +## 1. Core Concept: Server-less Freedom +P2P synchronisation allows your devices to talk directly to each other using WebRTC. A central server is not required for data storage, ensuring maximum privacy and "freedom." + +## 2. Setting Up via P2P Status Pane +You no longer need to navigate through complex menus. Simply open the **P2P Server Status** (via the ribbon icon or command palette) and click the **⚙ (Cog)** icon. + +This opens the **P2P Setup** dialogue where you can configure the essentials: +- **Room ID:** A unique identifier for your synchronisation group. +- **Password:** Your encryption key. Ensure all your devices use the exact same password. +- **Device Name:** A recognisable name for the current device (e.g., `iphone-16`). + +Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. + +*Tip: You can also toggle **Auto Connect** in the setup dialogue to automatically join the network whenever Obsidian starts.* + +## 3. Real-time Control +The status pane in the right sidebar provides granular control over your synchronisation: + +- **Signalling Status:** Shows if you are connected to the relay (🟢 Online). +- **Live-push (Broadcast):** Toggle "Broadcast changes" to notify other peers whenever you make an edit. +- **Watch:** Enable "Watch" on specific peers to automatically pull changes when they broadcast. This creates a "LiveSync-like" experience. + +## 4. Enhanced Replication Dialogue (Bidirectional Sync) +If you want to synchronise manually, click the **🔄 (Replicate)** button next to a peer in the device list. This opens the **Replication Dialogue**. + +Inside the dialogue, you can still see the **Server Status** at the top, so you will know if you are still connected while performing manual synchronisations. + +When you trigger a synchronisation this way, the system now performs a **Bidirectional Synchronisation**: +1. **Pull:** It first fetches changes from the peer. +2. **Push:** If the pull is successful, it immediately pushes your local changes to that peer. + +This "one-click" approach ensures both devices are perfectly in synchronisation without manual back-and-forth. + +## 5. Technical Improvements in 2026 +- **Decoupled Architecture:** The UI is now strictly separated from the core logic, making the plugin more stable across different platforms (Mobile, Desktop, and Web). +- **Svelte 5 UI:** The interface has been rebuilt for better responsiveness and clearer status indicators. +- **Security:** All data remains end-to-end encrypted. Even the signalling relay never sees your actual notes. + diff --git a/manifest.json b/manifest.json index dded2c4..66379e4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { - "id": "obsidian-livesync", - "name": "Self-hosted LiveSync", - "version": "0.25.62", - "minAppVersion": "1.7.2", - "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", - "author": "vorotamoroz", - "authorUrl": "https://github.com/vrtmrz", - "isDesktopOnly": false -} \ No newline at end of file + "id": "obsidian-livesync", + "name": "Self-hosted LiveSync", + "version": "0.25.62", + "minAppVersion": "1.7.2", + "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", + "author": "vorotamoroz", + "authorUrl": "https://github.com/vrtmrz", + "isDesktopOnly": false +} diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts b/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts new file mode 100644 index 0000000..8be0ab3 --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts @@ -0,0 +1,69 @@ +import { App, Modal } from "@/deps.ts"; +import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte"; +import { mount, unmount } from "svelte"; +import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; + +export type P2POpenReplicationModalCallback = { + onSync: (peerId: string) => Promise; + onSyncAndClose: (peerId: string) => Promise; +}; + +export class P2POpenReplicationModal extends Modal { + liveSyncReplicator: LiveSyncTrysteroReplicator; + callback?: P2POpenReplicationModalCallback; + component?: ReturnType; + showResult: boolean; + + constructor( + app: App, + liveSyncReplicator: LiveSyncTrysteroReplicator, + callback?: P2POpenReplicationModalCallback, + showResult: boolean = false + ) { + super(app); + this.liveSyncReplicator = liveSyncReplicator; + this.callback = callback; + this.showResult = showResult; + } + + async onSync(peerId: string) { + if (this.callback?.onSync) { + await this.callback.onSync(peerId); + } + } + + async onSyncAndClose(peerId: string) { + if (this.callback?.onSyncAndClose) { + await this.callback.onSyncAndClose(peerId); + } + this.close(); + } + + override onOpen() { + const { contentEl } = this; + this.titleEl.setText("P2P Replication"); + contentEl.empty(); + + if (this.component === undefined) { + this.component = mount(P2POpenReplicationPane, { + target: contentEl, + props: { + liveSyncReplicator: this.liveSyncReplicator, + onSync: (peerId: string) => this.onSync(peerId), + onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId), + onClose: () => this.close(), + showResult: this.showResult, + }, + }); + } + } + + override onClose() { + const { contentEl } = this; + contentEl.empty(); + if (this.component !== undefined) { + void unmount(this.component); + this.component = undefined; + } + } +} diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte new file mode 100644 index 0000000..8247b67 --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte @@ -0,0 +1,288 @@ + + +
+ + +
+

Available Devices

+ {#if serverInfo && serverInfo.knownAdvertisements.length > 0} +
+ {#each serverInfo.knownAdvertisements as peer (peer.peerId)} +
+
+
{peer.name}
+
+ {peer.platform} + + {peer.peerId.slice(0, 8)} + + + {getAcceptanceStatus(peer)} + +
+
+
+ + +
+
+ {/each} +
+ {:else if serverInfo} +

No devices available. Waiting for other devices to connect...

+ {/if} +
+ + +
+ + diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts new file mode 100644 index 0000000..28907db --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts @@ -0,0 +1,73 @@ +import { App } from "@/deps.ts"; +import { Logger } from "@lib/common/logger"; +import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types"; +import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; +import { P2POpenReplicationModal } from "./P2POpenReplicationModal"; + +/** + * Creates an openReplicationUI factory for Obsidian environments. + * Returns a per-replicator closure that opens the P2P Replication modal + * and performs bidirectional sync (pull then push on success). + * + * Usage: + * const factory = createOpenReplicationUI(app); + * useP2PReplicatorFeature(core, factory); + */ +export function createOpenReplicationUI( + app: App +): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise { + return (replicator: LiveSyncTrysteroReplicator) => + (showResult: boolean): Promise => { + const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; + return new Promise((resolve) => { + const modal = new P2POpenReplicationModal( + app, + replicator, + { + onSync: async (peerId: string) => { + try { + // pull (replicateFrom) first; push only on success + const pullResult = await replicator.replicateFrom(peerId, showResult); + if (pullResult?.ok) { + const pushResult = await replicator.requestSynchroniseToPeer(peerId); + resolve(pushResult?.ok ?? true); + } else { + resolve(false); + } + } catch (e) { + Logger( + `Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`, + logLevel + ); + resolve(false); + } + }, + onSyncAndClose: async (peerId: string) => { + try { + const pullResult = await replicator.replicateFrom(peerId, showResult); + if (pullResult?.ok) { + const pushResult = await replicator.requestSynchroniseToPeer(peerId); + if (pushResult?.ok ?? true) { + await replicator.close(); + resolve(true); + } else { + resolve(false); + } + } else { + resolve(false); + } + } catch (e) { + Logger( + `Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`, + logLevel + ); + resolve(false); + } + }, + }, + showResult + ); + modal.open(); + }); + }; +} diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte new file mode 100644 index 0000000..cf64a30 --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte @@ -0,0 +1,201 @@ + + +
+

Signalling Status

+ +
+ Connection: + + {isConnected ? "🟢 Connected" : "🔴 Disconnected"} + +
+ +
+ {#if !isConnected} + + {:else} + + {/if} +
+ + {#if serverInfo} +
+ Peer ID: + + {serverInfo.serverPeerId.slice(0, 12)}... + +
+ +
+ Devices: + {serverInfo.knownAdvertisements.length} +
+ {/if} + + {#if showBroadcastToggle} +
+ + + +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte new file mode 100644 index 0000000..cfcc206 --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte @@ -0,0 +1,558 @@ + + +
+
+

P2P Host

+ +
+ + + +
+
+

Known Devices

+ +
+ + {#if serverInfo && serverInfo.knownAdvertisements.length > 0} +
+ {#each serverInfo.knownAdvertisements as peer (peer.peerId)} +
+
+
+ {peer.name} : ({peer.peerId.slice(0, 8)}) + {#if isCommunicating(peer.peerId)} + 📡 + {/if} +
+
+ {peer.platform} +
+
+
+ {#if isAccepted(peer)} +
+ + {getAcceptanceStatus(peer)} + + +
+
+ WATCH + +
+ {:else} +
+ + {getAcceptanceStatus(peer)} + +
+
+ PERMANENT + + +
+
+ SESSION + + +
+ {/if} + {#if !isAccepted(peer) && (peer.isAccepted !== undefined || peer.isTemporaryAccepted !== undefined)} + + {/if} +
+
+ {/each} +
+ {:else if serverInfo} +

No devices available. Waiting for other devices to connect...

+ {:else} +

Fetching status...

+ {/if} +
+
+ + \ No newline at end of file diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts new file mode 100644 index 0000000..d6cc66a --- /dev/null +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts @@ -0,0 +1,42 @@ +import { WorkspaceLeaf } from "@/deps.ts"; +import { mount } from "svelte"; +import { SvelteItemView } from "@/common/SvelteItemView.ts"; +import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; +import type { P2PPaneParams } from "@/lib/src/replication/trystero/UseP2PReplicatorResult"; +import P2PServerStatusPane from "./P2PServerStatusPane.svelte"; + +export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status"; + +export class P2PServerStatusPaneView extends SvelteItemView { + core: LiveSyncBaseCore; + private _p2pResult: P2PPaneParams; + override icon = "waypoints"; + override navigation = false; + + constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams) { + super(leaf); + this.core = core; + this._p2pResult = p2pResult; + } + + override getIcon(): string { + return "waypoints"; + } + + getViewType() { + return VIEW_TYPE_P2P_SERVER_STATUS; + } + + getDisplayText() { + return "P2P Server Status"; + } + + instantiateComponent(target: HTMLElement) { + return mount(P2PServerStatusPane, { + target, + props: { + liveSyncReplicator: this._p2pResult.replicator, + }, + }); + } +} diff --git a/src/lib b/src/lib index ed4502e..2868aae 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit ed4502e0035bfee88eca5f311d09ffc239ab9734 +Subproject commit 2868aae6fdd9ca7738f4a69c88fa6d7a01842e4d diff --git a/src/main.ts b/src/main.ts index 63242b0..102d120 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,7 @@ import { useSetupManagerHandlersFeature } from "./serviceFeatures/setupObsidian/ import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature.ts"; import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands.ts"; import { useP2PReplicatorUI } from "./serviceFeatures/useP2PReplicatorUI.ts"; +import { createOpenReplicationUI } from "./features/P2PSync/P2PReplicator/P2PReplicationUI.ts"; export type LiveSyncCore = LiveSyncBaseCore; export default class ObsidianLiveSyncPlugin extends Plugin { core: LiveSyncCore; @@ -176,7 +177,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin { const curriedFeature = () => featuresInitialiser(core); core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature); const setupManager = core.getModule(SetupManager); - + const replicator = useP2PReplicatorFeature(core, createOpenReplicationUI(this.app)); + useP2PReplicatorCommands(core, replicator); + useP2PReplicatorUI(core, core, replicator); useRemoteConfiguration(core); useSetupProtocolFeature(core, setupManager); @@ -190,9 +193,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin { // VIEW_TYPE_P2P, // (leaf: any) => new P2PReplicatorPaneView(leaf, core, p2pReplicatorResult!), // ]); - const replicator = useP2PReplicatorFeature(core); - useP2PReplicatorCommands(core, replicator); - useP2PReplicatorUI(core, core, replicator); } ); } diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index 79b7b49..ddc59d0 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -1,3 +1,4 @@ +import type PouchDB from "pouchdb-core"; import { fireAndForget } from "octagonal-wheels/promises"; import { AbstractModule } from "../AbstractModule"; import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "octagonal-wheels/common/logger"; diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte index b07c157..64a794f 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte @@ -39,18 +39,20 @@ const { setResult, getInitialData }: Props = $props(); onMount(() => { + let initialData: P2PSyncSetting | undefined = undefined; if (getInitialData) { - const initialData = getInitialData(); + initialData = getInitialData(); if (initialData) { copyTo(initialData, syncSetting); } - if (context.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME)) { - syncSetting.P2P_DevicePeerName = context.services.config.getSmallConfig( - SETTING_KEY_P2P_DEVICE_NAME - ) as string; - } else { - syncSetting.P2P_DevicePeerName = ""; - } + } + const initialPeerName = (initialData?.P2P_DevicePeerName ?? "").trim(); + if (initialPeerName !== "") { + return; + } + const cachedPeerName = context.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME); + if (cachedPeerName) { + syncSetting.P2P_DevicePeerName = cachedPeerName as string; } }); function generateSetting() { @@ -100,7 +102,7 @@ const dummyPouch = new PouchDB("dummy"); const env: ReplicatorHostEnv = { settings: trialRemoteSetting, - processReplicatedDocs: async (docs: any[]) => { + processReplicatedDocs: async (_docs: any[]) => { return; }, confirm: context.services.confirm, @@ -116,7 +118,7 @@ await replicator.open(); for (let i = 0; i < 10; i++) { // await delay(1000); - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => window.setTimeout(resolve, 1000)); // Logger(`Checking known advertisements... (${i})`, LOG_LEVEL_INFO); if (replicator.knownAdvertisements.length > 0) { break; diff --git a/src/modules/services/ObsidianAPIService.ts b/src/modules/services/ObsidianAPIService.ts index f543c30..19784e4 100644 --- a/src/modules/services/ObsidianAPIService.ts +++ b/src/modules/services/ObsidianAPIService.ts @@ -38,6 +38,20 @@ export class ObsidianAPIService extends InjectableAPIService { + const rightLeaf = this.app.workspace.getRightLeaf(false); + if (rightLeaf) { + await rightLeaf.setViewState({ + type: viewType, + active: false, + }); + await this.app.workspace.revealLeaf(rightLeaf); + return; + } + + await this.showWindow(viewType); + } + private get app() { return this.context.app; } diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index a2ea2d1..b55a895 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -4,6 +4,10 @@ import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; import { type UseP2PReplicatorResult } from "@/lib/src/replication/trystero/UseP2PReplicatorResult"; import { P2PLogCollector } from "@/lib/src/replication/trystero/P2PLogCollector"; import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView"; +import { + P2PServerStatusPaneView, + VIEW_TYPE_P2P_SERVER_STATUS, +} from "@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView"; import type { LiveSyncCore } from "@/main"; import type { WorkspaceLeaf } from "@/deps"; @@ -34,6 +38,19 @@ export function useP2PReplicatorUI( core: LiveSyncCore, replicator: UseP2PReplicatorResult ) { + const api = host.services.API as { + showWindow: (type: string) => Promise; + showWindowOnRight?: (type: string) => Promise; + registerWindow: (type: string, factory: (leaf: WorkspaceLeaf) => unknown) => void; + addCommand: (command: { id: string; name: string; callback: () => void }) => unknown; + addRibbonIcon: ( + icon: string, + title: string, + callback: () => void + ) => { addClass?: (name: string) => unknown } | undefined; + getPlatform: () => string; + }; + // const env: LiveSyncTrysteroReplicatorEnv = { services: host.services as any }; const getReplicator = () => replicator.replicator; const p2pLogCollector = new P2PLogCollector(); @@ -51,26 +68,64 @@ export function useP2PReplicatorUI( storeP2PStatusLine, }); }; - const openPane = () => host.services.API.showWindow(viewType); - host.services.API.registerWindow(viewType, factory); + const statusFactory = (leaf: WorkspaceLeaf) => { + return new P2PServerStatusPaneView(leaf, core, { + replicator: getReplicator(), + p2pLogCollector, + storeP2PStatusLine, + }); + }; + const openPane = () => api.showWindow(viewType); + const openStatusPane = () => { + if (api.showWindowOnRight) { + return api.showWindowOnRight(VIEW_TYPE_P2P_SERVER_STATUS); + } + return api.showWindow(VIEW_TYPE_P2P_SERVER_STATUS); + }; + api.registerWindow(viewType, factory); + api.registerWindow(VIEW_TYPE_P2P_SERVER_STATUS, statusFactory); host.services.appLifecycle.onInitialise.addHandler(() => { eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => { void openPane(); }); - host.services.API.addCommand({ + api.addCommand({ id: "open-p2p-replicator", - name: "P2P Sync : Open P2P Replicator", + name: "P2P Sync : Open P2P Replicator (Old UI)", callback: () => { void openPane(); }, }); - host.services.API.addRibbonIcon("waypoints", "P2P Replicator", () => { - void openPane(); - })?.addClass?.("livesync-ribbon-replicate-p2p"); + api.addCommand({ + id: "open-p2p-server-status", + name: "P2P Sync : Open P2P Server Status", + callback: () => { + void openStatusPane(); + }, + }); + // api.addRibbonIcon("waypoints", "P2P Replicator", () => { + // void openPane(); + // })?.addClass?.("livesync-ribbon-replicate-p2p"); + + api.addRibbonIcon("waypoints", "P2P Server Status", () => { + void openStatusPane(); + })?.addClass?.("livesync-ribbon-p2p-server-status"); + + return Promise.resolve(true); + }); + + host.services.appLifecycle.onLayoutReady.addHandler(() => { + if (api.getPlatform() !== "obsidian") { + return Promise.resolve(true); + } + if (api.showWindowOnRight) { + void api.showWindowOnRight(VIEW_TYPE_P2P_SERVER_STATUS); + } else { + void api.showWindow(VIEW_TYPE_P2P_SERVER_STATUS); + } return Promise.resolve(true); }); return { replicator: getReplicator(), p2pLogCollector, storeP2PStatusLine }; diff --git a/updates.md b/updates.md index 621065b..a392a7f 100644 --- a/updates.md +++ b/updates.md @@ -3,6 +3,18 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +## Unreleased + +15th May, 2026 + + +### Fixed +- The issue which cannot synchronise in Only-P2P mode has been fixed. + +### P2P Replication UI Improvements +- Brand-new P2P Server Status pane has been added to provide real-time visibility into your connection status and peer network. +- Now `Replicate` button or ribbon icon opens a redesigned interactive replication dialogue that performs smart bidirectional sync with a single click. + ## 0.25.62 14th May, 2026 From f0628a0d2c64b273a875269b6d7288b7a64d8601 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 16 May 2026 23:09:11 +0900 Subject: [PATCH 2/7] Improve UI --- docs/p2p_sync_updates_2026.md | 4 ++-- src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte | 2 +- src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte | 4 ++-- src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts | 2 +- src/lib | 2 +- src/serviceFeatures/useP2PReplicatorUI.ts | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md index 5a8f9e4..13acf46 100644 --- a/docs/p2p_sync_updates_2026.md +++ b/docs/p2p_sync_updates_2026.md @@ -6,11 +6,11 @@ Peer-to-Peer (P2P) synchronisation has evolved significantly. This guide covers P2P synchronisation allows your devices to talk directly to each other using WebRTC. A central server is not required for data storage, ensuring maximum privacy and "freedom." ## 2. Setting Up via P2P Status Pane -You no longer need to navigate through complex menus. Simply open the **P2P Server Status** (via the ribbon icon or command palette) and click the **⚙ (Cog)** icon. +You no longer need to navigate through complex menus. Simply open the **P2P Status** (via the ribbon icon or command palette) and click the **⚙ (Cog)** icon. This opens the **P2P Setup** dialogue where you can configure the essentials: - **Room ID:** A unique identifier for your synchronisation group. -- **Password:** Your encryption key. Ensure all your devices use the exact same password. +- **Passphrase:** Your encryption key. Ensure all your devices use the exact same passphrase. - **Device Name:** A recognisable name for the current device (e.g., `iphone-16`). Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte index cf64a30..980aed4 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte @@ -21,7 +21,7 @@ let replicatorStatus = $state(undefined); async function requestServerStatus() { - await liveSyncReplicator.requestStatus(); + await Promise.resolve(liveSyncReplicator.requestStatus()); eventHub.emitEvent(EVENT_REQUEST_STATUS); } diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte index cfcc206..2952146 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte @@ -149,7 +149,7 @@
-

P2P Host

+

P2P Status

diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts index d6cc66a..99d2347 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts @@ -28,7 +28,7 @@ export class P2PServerStatusPaneView extends SvelteItemView { } getDisplayText() { - return "P2P Server Status"; + return "P2P Status"; } instantiateComponent(target: HTMLElement) { diff --git a/src/lib b/src/lib index 2868aae..bf060df 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 2868aae6fdd9ca7738f4a69c88fa6d7a01842e4d +Subproject commit bf060df09183f2992829f0e8ec19bb6e389c1613 diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index b55a895..59ef277 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -100,7 +100,7 @@ export function useP2PReplicatorUI( api.addCommand({ id: "open-p2p-server-status", - name: "P2P Sync : Open P2P Server Status", + name: "P2P Sync : Open P2P Status", callback: () => { void openStatusPane(); }, @@ -110,7 +110,7 @@ export function useP2PReplicatorUI( // void openPane(); // })?.addClass?.("livesync-ribbon-replicate-p2p"); - api.addRibbonIcon("waypoints", "P2P Server Status", () => { + api.addRibbonIcon("waypoints", "P2P Status", () => { void openStatusPane(); })?.addClass?.("livesync-ribbon-p2p-server-status"); From 9a90256a8a9dca35bd2b1c627884d26c1211df15 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 16 May 2026 23:50:08 +0900 Subject: [PATCH 3/7] Enhance P2P synchronization features and UI improvements --- docs/p2p_sync_updates_2026.md | 29 +++++++---- .../P2POpenReplicationPane.svelte | 27 +++++----- .../P2PReplicator/P2PServerStatusPane.svelte | 51 +++++++++++++++++-- .../P2PReplicator/P2PServerStatusPaneView.ts | 1 + src/lib | 2 +- .../dialogs/SelectMethodNewUser.svelte | 2 + src/serviceFeatures/useP2PReplicatorUI.ts | 36 +++++++++++++ 7 files changed, 122 insertions(+), 26 deletions(-) diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md index 13acf46..34dc9de 100644 --- a/docs/p2p_sync_updates_2026.md +++ b/docs/p2p_sync_updates_2026.md @@ -13,7 +13,7 @@ This opens the **P2P Setup** dialogue where you can configure the essentials: - **Passphrase:** Your encryption key. Ensure all your devices use the exact same passphrase. - **Device Name:** A recognisable name for the current device (e.g., `iphone-16`). -Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. +Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. *Tip: You can also toggle **Auto Connect** in the setup dialogue to automatically join the network whenever Obsidian starts.* @@ -23,19 +23,30 @@ The status pane in the right sidebar provides granular control over your synchro - **Signalling Status:** Shows if you are connected to the relay (🟢 Online). - **Live-push (Broadcast):** Toggle "Broadcast changes" to notify other peers whenever you make an edit. - **Watch:** Enable "Watch" on specific peers to automatically pull changes when they broadcast. This creates a "LiveSync-like" experience. +- **Sync (🔄/🔁):** Mark specific peers as **sync targets**. Peers marked here will be included when you run the **"P2P: Sync with targets"** command (see section 5). Click the button next to a peer to toggle it on (🔄, highlighted) or off (🔁). This setting is persisted in your configuration. -## 4. Enhanced Replication Dialogue (Bidirectional Sync) -If you want to synchronise manually, click the **🔄 (Replicate)** button next to a peer in the device list. This opens the **Replication Dialogue**. +## 4. Replication Dialogue +If you want to synchronise with a specific peer manually, use the **Replication** command or button. This opens the **Replication Dialogue** listing available devices. -Inside the dialogue, you can still see the **Server Status** at the top, so you will know if you are still connected while performing manual synchronisations. +Inside the dialogue, the **Server Status** card at the top confirms you are still connected while performing the sync. -When you trigger a synchronisation this way, the system now performs a **Bidirectional Synchronisation**: -1. **Pull:** It first fetches changes from the peer. -2. **Push:** If the pull is successful, it immediately pushes your local changes to that peer. +Two actions are available per peer: -This "one-click" approach ensures both devices are perfectly in synchronisation without manual back-and-forth. +- **Sync** — Starts a bidirectional synchronisation (Pull then Push) and keeps the dialogue open so you can monitor progress or sync with additional peers. +- **Start Sync & Close** — Starts the same bidirectional sync in the background and **immediately closes the dialogue**, so you can continue working without waiting. -## 5. Technical Improvements in 2026 +## 5. Syncing with Registered Targets via Command Palette + +You can now trigger a synchronisation with all your pre-registered target peers in one step, without opening any UI. + +1. Open the **Command Palette** (`Ctrl/Cmd + P`). +2. Run **"P2P: Sync with targets"**. + +This command synchronises with every peer whose **SYNC** toggle is enabled in the **Detected Peers** list. If no targets are registered, or if the P2P server is not running, the command will notify you accordingly. + +*Tip: Pair this command with a hotkey for a quick, keyboard-driven sync workflow.* + +## 6. Technical Improvements in 2026 - **Decoupled Architecture:** The UI is now strictly separated from the core logic, making the plugin more stable across different platforms (Mobile, Desktop, and Web). - **Svelte 5 UI:** The interface has been rebuilt for better responsiveness and clearer status indicators. - **Security:** All data remains end-to-end encrypted. Even the signalling relay never sees your actual notes. diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte index 8247b67..c745891 100644 --- a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte @@ -57,16 +57,16 @@ } async function handleSyncAndClose(peerId: string) { - try { - syncingPeerId = peerId; - Logger(`Starting sync and close with ${peerId}`, logLevel); - await onSyncAndClose(peerId); - Logger(`Sync and close completed with ${peerId}`, logLevel); - } catch (e) { - Logger(`Error during sync and close: ${e instanceof Error ? e.message : String(e)}`, logLevel); - } finally { - syncingPeerId = null; - } + fireAndForget(async () => { + try { + Logger(`Starting sync with ${peerId}`, logLevel); + await onSync(peerId); + Logger(`Sync completed with ${peerId}`, logLevel); + } catch (e) { + Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel); + } + }); + onClose(); } async function disconnect(){ try { @@ -103,7 +103,7 @@ />
-

Available Devices

+

Available Peers

{#if serverInfo && serverInfo.knownAdvertisements.length > 0}
{#each serverInfo.knownAdvertisements as peer (peer.peerId)} @@ -133,7 +133,7 @@ disabled={syncingPeerId !== null} onclick={() => handleSyncAndClose(peer.peerId)} > - {syncingPeerId === peer.peerId ? "Syncing..." : "Sync & Close"} + Start Sync & Close
@@ -181,6 +181,7 @@ .peer-item { display: flex; justify-content: space-between; + flex-wrap: wrap; align-items: center; padding: 0.75rem; background-color: var(--background-secondary); @@ -234,9 +235,9 @@ } .peer-actions { + flex-wrap: wrap; display: flex; gap: 0.5rem; - flex-shrink: 0; } .btn { diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte index 2952146..fc148f8 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte @@ -12,17 +12,32 @@ import type { P2PReplicatorStatus, P2PReplicationReport } from "@/lib/src/replication/trystero/TrysteroReplicator"; import { delay, fireAndForget } from "octagonal-wheels/promises"; import P2PServerStatusCard from "./P2PServerStatusCard.svelte"; + import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents"; + import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; interface Props { liveSyncReplicator: LiveSyncTrysteroReplicator; + core: LiveSyncBaseCore; } - let { liveSyncReplicator }: Props = $props(); + let { liveSyncReplicator, core }: Props = $props(); let serverInfo = $state(undefined); let replicatorInfo = $state(undefined); let decidingPeerId = $state(null); let communicatingUntil = $state>({}); const COMMUNICATION_HOLD_MS = 2500; + let syncOnReplicationSetting = $state( + core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? "" + ); + + function addToList(item: string, list: string): string { + const items = list.split(",").map((e) => e.trim()).filter((e) => e); + if (!items.includes(item)) items.push(item); + return items.join(","); + } + function removeFromList(item: string, list: string): string { + return list.split(",").map((e) => e.trim()).filter((e) => e && e !== item).join(","); + } function markCommunicating(peerId: string) { const expiry = Date.now() + COMMUNICATION_HOLD_MS; @@ -60,6 +75,10 @@ } }); + const unsubscribeSettings = eventHub.onEvent(EVENT_SETTING_SAVED, (settings) => { + syncOnReplicationSetting = settings?.P2P_SyncOnReplication ?? ""; + }); + fireAndForget(async () => { await delay(100); await requestServerStatus(); @@ -69,6 +88,7 @@ unsubscribe(); unsubscribeReplicatorStatus(); unsubscribeReplicatorProgress(); + unsubscribeSettings(); }; }); @@ -145,6 +165,22 @@ const isHeldCommunicating = (communicatingUntil[peerId] ?? 0) > Date.now(); return isLiveCommunicating || isHeldCommunicating; } + + function isSyncTarget(peerName: string) { + return syncOnReplicationSetting + .split(",") + .map((e) => e.trim()) + .filter((e) => e) + .includes(peerName); + } + + async function toggleSyncTarget(peer: P2PServerInfo["knownAdvertisements"][number]) { + const currentValue = core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? ""; + const newValue = isSyncTarget(peer.name) + ? removeFromList(peer.name, currentValue) + : addToList(peer.name, currentValue); + await core.services.setting.applyPartial({ P2P_SyncOnReplication: newValue }, true); + }
@@ -207,8 +243,17 @@ > {isWatching(peer.peerId) ? '👁' : '👁‍🗨'} -
- {:else} +
+ SYNC + +
{:else}
{getAcceptanceStatus(peer)} diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts index 99d2347..a182b8a 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts @@ -36,6 +36,7 @@ export class P2PServerStatusPaneView extends SvelteItemView { target, props: { liveSyncReplicator: this._p2pResult.replicator, + core: this.core, }, }); } diff --git a/src/lib b/src/lib index bf060df..4ce9136 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit bf060df09183f2992829f0e8ec19bb6e389c1613 +Subproject commit 4ce9136a434fe540ee25ee48568a510703a31b1a diff --git a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte index 2478521..b4dd9ea 100644 --- a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte +++ b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte @@ -48,6 +48,8 @@ bind:value={userType} > This is an advanced option for users who do not have a URI or who wish to configure detailed settings. + You can also select this option if you intend to use P2P (Peer-to-Peer) synchronisation + instead of a CouchDB/S3 server — P2P requires no server setup at all. diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index 59ef277..3e3882a 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -10,6 +10,7 @@ import { } from "@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView"; import type { LiveSyncCore } from "@/main"; import type { WorkspaceLeaf } from "@/deps"; +import { REMOTE_P2P } from "@lib/common/models/setting.const"; /** * ServiceFeature: P2P Replicator lifecycle management. @@ -105,6 +106,41 @@ export function useP2PReplicatorUI( void openStatusPane(); }, }); + host.services.API.addCommand({ + id: "replicate-now-by-p2p-default-peer", + name: "Replicate P2P to default peer", + checkCallback: (isChecking: boolean) => { + const settings = host.services.setting.currentSettings(); + if (isChecking) { + if (settings.remoteType == REMOTE_P2P) return false; + return replicator.replicator?.server?.isServing ?? false; + } + void replicator.replicator?.openReplication(settings, false, true, false); + }, + }); + host.services.API.addCommand({ + id: "replicate-now-by-p2p", + name: "Replicate now by P2P", + checkCallback: (isChecking: boolean) => { + const settings = host.services.setting.currentSettings(); + if (isChecking) { + if (settings.remoteType == REMOTE_P2P) return false; + return replicator.replicator?.server?.isServing ?? false; + } + void replicator.replicator?.openReplication(settings, false, true, false); + }, + }); + + host.services.API.addCommand({ + id: "p2p-sync-targets", + name: "P2P: Sync with targets", + checkCallback: (isChecking: boolean) => { + if (isChecking) { + return replicator.replicator?.server?.isServing ?? false; + } + void replicator.replicator?.replicateFromCommand(true); + }, + }); // api.addRibbonIcon("waypoints", "P2P Replicator", () => { // void openPane(); From 4ed1749652c5534409e8016a32310bf0ea29224c Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 17 May 2026 01:36:09 +0900 Subject: [PATCH 4/7] Enhance P2P synchronization features and UI improvements --- .../P2PReplicator/P2POpenReplicationModal.ts | 15 +++- .../P2POpenReplicationPane.svelte | 74 ++++++++++++------- .../P2PSync/P2PReplicator/P2PReplicationUI.ts | 60 ++++++++++++++- src/lib | 2 +- src/main.ts | 8 +- .../ModuleResolveMismatchedTweaks.ts | 4 + src/modules/services/ObsidianAPIService.ts | 5 ++ src/serviceFeatures/redFlag.ts | 9 ++- 8 files changed, 145 insertions(+), 32 deletions(-) diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts b/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts index 8be0ab3..aa2cd94 100644 --- a/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.ts @@ -13,17 +13,26 @@ export class P2POpenReplicationModal extends Modal { callback?: P2POpenReplicationModalCallback; component?: ReturnType; showResult: boolean; + title: string; + onClosed?: () => void; + rebuildMode: boolean; constructor( app: App, liveSyncReplicator: LiveSyncTrysteroReplicator, callback?: P2POpenReplicationModalCallback, - showResult: boolean = false + showResult: boolean = false, + title: string = "P2P Replication", + onClosed?: () => void, + rebuildMode: boolean = false ) { super(app); this.liveSyncReplicator = liveSyncReplicator; this.callback = callback; this.showResult = showResult; + this.title = title; + this.onClosed = onClosed; + this.rebuildMode = rebuildMode; } async onSync(peerId: string) { @@ -41,7 +50,7 @@ export class P2POpenReplicationModal extends Modal { override onOpen() { const { contentEl } = this; - this.titleEl.setText("P2P Replication"); + this.titleEl.setText(this.title); contentEl.empty(); if (this.component === undefined) { @@ -53,6 +62,7 @@ export class P2POpenReplicationModal extends Modal { onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId), onClose: () => this.close(), showResult: this.showResult, + rebuildMode: this.rebuildMode, }, }); } @@ -65,5 +75,6 @@ export class P2POpenReplicationModal extends Modal { void unmount(this.component); this.component = undefined; } + this.onClosed?.(); } } diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte index c745891..58c7e99 100644 --- a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte @@ -19,9 +19,10 @@ onSyncAndClose: (_peerId: string) => Promise; onClose: () => void; showResult: boolean; + rebuildMode?: boolean; } - let { onSync, onSyncAndClose, onClose, showResult, liveSyncReplicator }: Props = $props(); + let { onSync, onSyncAndClose, onClose, showResult, liveSyncReplicator, rebuildMode = false }: Props = $props(); let serverInfo = $state(undefined); let syncingPeerId = $state(null); @@ -36,10 +37,10 @@ const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => { serverInfo = status; }); - fireAndForget(async ()=>{ + fireAndForget(async () => { await delay(100); await requestServerStatus(); - }) + }); return unsubscribe; }); @@ -55,6 +56,18 @@ syncingPeerId = null; } } + async function handleSyncThenClose(peerId: string) { + try { + syncingPeerId = peerId; + Logger(`Starting sync with ${peerId}`, logLevel); + await onSyncAndClose(peerId); + Logger(`Sync completed with ${peerId}`, logLevel); + } catch (e) { + Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel); + } finally { + syncingPeerId = null; + } + } async function handleSyncAndClose(peerId: string) { fireAndForget(async () => { @@ -68,7 +81,7 @@ }); onClose(); } - async function disconnect(){ + async function disconnect() { try { await liveSyncReplicator.close(); Logger("Signalling connection closed.", logLevel); @@ -76,7 +89,7 @@ Logger(`Failed to close signalling connection: ${e instanceof Error ? e.message : String(e)}`, logLevel); } } - async function onCloseAndDisconnect(){ + async function onCloseAndDisconnect() { await disconnect(); onClose(); } @@ -97,10 +110,7 @@
- +

Available Peers

@@ -121,20 +131,30 @@
- - + {#if !rebuildMode} + + + {:else} + + {/if}
{/each} @@ -145,8 +165,12 @@ diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts index 28907db..ae5f02f 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts +++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts @@ -1,7 +1,7 @@ import { App } from "@/deps.ts"; import { Logger } from "@lib/common/logger"; import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types"; -import type { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; +import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; import { P2POpenReplicationModal } from "./P2POpenReplicationModal"; /** @@ -71,3 +71,61 @@ export function createOpenReplicationUI( }); }; } + +/** + * Creates an openRebuildUI factory for Obsidian environments. + * Opens the P2P Replication modal in "rebuild" mode — one-way pull only, + * with setOnSetup / clearOnSetup bracketing the replicateFrom call. + * + * Usage: + * const factory = createOpenRebuildUI(app); + * useP2PReplicatorFeature(core, createOpenReplicationUI(app), factory); + */ +export function createOpenRebuildUI( + app: App +): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise { + return (replicator: LiveSyncTrysteroReplicator) => + (showResult: boolean): Promise => { + const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; + return new Promise((resolve) => { + let resolved = false; + const safeResolve = (val: boolean) => { + if (!resolved) { + resolved = true; + resolve(val); + } + }; + + const doRebuild = async (peerId: string) => { + replicator.setOnSetup(); + try { + Logger(`Rebuilding from peer ${peerId}`, logLevel); + const result = await replicator.replicateFrom(peerId, showResult); + safeResolve(result?.ok ?? false); + } catch (e) { + Logger( + `Error in rebuild from ${peerId}: ${e instanceof Error ? e.message : String(e)}`, + logLevel + ); + safeResolve(false); + } finally { + replicator.clearOnSetup(); + } + }; + + const modal = new P2POpenReplicationModal( + app, + replicator, + { + onSync: doRebuild, + onSyncAndClose: doRebuild, + }, + showResult, + "P2P Rebuild", + () => safeResolve(false), + true + ); + modal.open(); + }); + }; +} diff --git a/src/lib b/src/lib index 4ce9136..cc552ac 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 4ce9136a434fe540ee25ee48568a510703a31b1a +Subproject commit cc552acad2995eb9f4eb470a2c09d447c3db867f diff --git a/src/main.ts b/src/main.ts index 102d120..6c5c3f3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,7 +44,7 @@ import { useSetupManagerHandlersFeature } from "./serviceFeatures/setupObsidian/ import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature.ts"; import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands.ts"; import { useP2PReplicatorUI } from "./serviceFeatures/useP2PReplicatorUI.ts"; -import { createOpenReplicationUI } from "./features/P2PSync/P2PReplicator/P2PReplicationUI.ts"; +import { createOpenReplicationUI, createOpenRebuildUI } from "./features/P2PSync/P2PReplicator/P2PReplicationUI.ts"; export type LiveSyncCore = LiveSyncBaseCore; export default class ObsidianLiveSyncPlugin extends Plugin { core: LiveSyncCore; @@ -177,7 +177,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin { const curriedFeature = () => featuresInitialiser(core); core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature); const setupManager = core.getModule(SetupManager); - const replicator = useP2PReplicatorFeature(core, createOpenReplicationUI(this.app)); + const replicator = useP2PReplicatorFeature( + core, + createOpenReplicationUI(this.app), + createOpenRebuildUI(this.app) + ); useP2PReplicatorCommands(core, replicator); useP2PReplicatorUI(core, core, replicator); useRemoteConfiguration(core); diff --git a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts index fab0091..d5776ef 100644 --- a/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts +++ b/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts @@ -14,6 +14,7 @@ import { AbstractModule } from "../AbstractModule.ts"; import { $msg } from "../../lib/src/common/i18n.ts"; import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; import type { LiveSyncCore } from "../../main.ts"; +import { REMOTE_P2P } from "@lib/common/models/setting.const.ts"; export class ModuleResolvingMismatchedTweaks extends AbstractModule { async _anyAfterConnectCheckFailed(): Promise { @@ -186,6 +187,9 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule { async _checkAndAskUseRemoteConfiguration( trialSetting: RemoteDBSettings ): Promise<{ result: false | TweakValues; requireFetch: boolean }> { + if (trialSetting.remoteType === REMOTE_P2P) { + return { result: false, requireFetch: false }; + } const preferred = await this.services.tweakValue.fetchRemotePreferred(trialSetting); if (preferred) { return await this.services.tweakValue.askUseRemoteConfiguration(trialSetting, preferred); diff --git a/src/modules/services/ObsidianAPIService.ts b/src/modules/services/ObsidianAPIService.ts index 19784e4..9f19c2d 100644 --- a/src/modules/services/ObsidianAPIService.ts +++ b/src/modules/services/ObsidianAPIService.ts @@ -39,6 +39,11 @@ export class ObsidianAPIService extends InjectableAPIService { + const existing = this.app.workspace.getLeavesOfType(viewType); + if (existing.length > 0) { + await this.app.workspace.revealLeaf(existing[0]); + return; + } const rightLeaf = this.app.workspace.getRightLeaf(false); if (rightLeaf) { await rightLeaf.setViewState({ diff --git a/src/serviceFeatures/redFlag.ts b/src/serviceFeatures/redFlag.ts index 90e052e..fa1dada 100644 --- a/src/serviceFeatures/redFlag.ts +++ b/src/serviceFeatures/redFlag.ts @@ -5,7 +5,7 @@ import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/re import FetchEverything from "../modules/features/SetupWizard/dialogs/FetchEverything.svelte"; import RebuildEverything from "../modules/features/SetupWizard/dialogs/RebuildEverything.svelte"; import { extractObject } from "octagonal-wheels/object"; -import { REMOTE_MINIO } from "@lib/common/models/setting.const"; +import { REMOTE_MINIO, REMOTE_P2P } from "@lib/common/models/setting.const"; import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; import { TweakValuesShouldMatchedTemplate } from "@lib/common/models/tweak.definition"; @@ -200,6 +200,13 @@ export async function adjustSettingToRemoteIfNeeded( return; } + // P2P has no centralised remote configuration; skip to avoid a spurious + // "Failed to connect to the remote server" error dialog. + if (config.remoteType === REMOTE_P2P) { + log("Remote configuration fetch skipped (P2P mode).", LOG_LEVEL_INFO); + return; + } + // Remote configuration fetched and applied. if (await adjustSettingToRemote(host, log, config)) { config = host.services.setting.currentSettings(); From a379b5bd7845fad88a1abbd3a8b74003f998f7b1 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 17 May 2026 01:40:50 +0900 Subject: [PATCH 5/7] bump --- manifest.json | 18 +++++++++--------- package-lock.json | 4 ++-- package.json | 2 +- src/lib | 2 +- updates.md | 22 +++++++++++++++------- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/manifest.json b/manifest.json index 66379e4..91eb189 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { - "id": "obsidian-livesync", - "name": "Self-hosted LiveSync", - "version": "0.25.62", - "minAppVersion": "1.7.2", - "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", - "author": "vorotamoroz", - "authorUrl": "https://github.com/vrtmrz", - "isDesktopOnly": false -} + "id": "obsidian-livesync", + "name": "Self-hosted LiveSync", + "version": "0.25.63", + "minAppVersion": "1.7.2", + "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", + "author": "vorotamoroz", + "authorUrl": "https://github.com/vrtmrz", + "isDesktopOnly": false +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4349282..68c1113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.25.62", + "version": "0.25.63", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.25.62", + "version": "0.25.63", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", diff --git a/package.json b/package.json index 4bbb2c6..4fd1805 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.25.62", + "version": "0.25.63", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "main": "main.js", "type": "module", diff --git a/src/lib b/src/lib index cc552ac..07e287c 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit cc552acad2995eb9f4eb470a2c09d447c3db867f +Subproject commit 07e287c53122a09300f916b5679826e2d1d75b5a diff --git a/updates.md b/updates.md index a392a7f..a5c9090 100644 --- a/updates.md +++ b/updates.md @@ -3,18 +3,26 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +## 0.25.63 + +17th May, 2026 + +### Fixed +- The issue which cannot synchronise in Only-P2P mode has been fixed. +- Fixed an issue where "Failed to connect to the remote server" was shown during the redFlag rebuild flow when P2P was the primary remote type. Remote configuration fetch is now skipped for P2P. + +### P2P Replication UI Improvements +- Brand-new P2P Server Status pane has been added to provide real-time visibility into your connection status and peer network. + - For detailed instructions on using the new P2P features, please refer to the updated [User Guide: Peer-to-Peer Synchronisation (2026 Edition)](./docs/p2p_sync_updates_2026.md). +- Now `Replicate` button or ribbon icon opens a redesigned interactive replication dialogue that performs smart bidirectional sync with a single click. +- The vault rebuild flow (`replicateAllFromServer`) now opens the redesigned P2P Replication modal instead of a plain text selection dialogue, providing a consistent UI experience. + + ## Unreleased 15th May, 2026 -### Fixed -- The issue which cannot synchronise in Only-P2P mode has been fixed. - -### P2P Replication UI Improvements -- Brand-new P2P Server Status pane has been added to provide real-time visibility into your connection status and peer network. -- Now `Replicate` button or ribbon icon opens a redesigned interactive replication dialogue that performs smart bidirectional sync with a single click. - ## 0.25.62 14th May, 2026 From 83228e2077c9ffb2ec866492dacec490eae5dde8 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 17 May 2026 02:23:58 +0900 Subject: [PATCH 6/7] Fix P2P replicator creation and enhance error handling in synchronization functions --- src/apps/cli/commands/p2p.ts | 17 +++++++++++------ src/apps/cli/main.ts | 11 +++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/apps/cli/commands/p2p.ts b/src/apps/cli/commands/p2p.ts index f47b62e..e3297ee 100644 --- a/src/apps/cli/commands/p2p.ts +++ b/src/apps/cli/commands/p2p.ts @@ -32,10 +32,15 @@ function validateP2PSettings(core: LiveSyncBaseCore) { settings.P2P_IsHeadless = true; } -function createReplicator(core: LiveSyncBaseCore): LiveSyncTrysteroReplicator { +async function createReplicator(core: LiveSyncBaseCore): Promise { validateP2PSettings(core); - const replicator = new LiveSyncTrysteroReplicator({ services: core.services }); - addP2PEventHandlers(replicator); + const replicator = await core.services.replicator.getNewReplicator(); + if (!replicator) { + throw new Error("Failed to create replicator instance. Ensure P2P is enabled in settings."); + } + if (!(replicator instanceof LiveSyncTrysteroReplicator)) { + throw new Error("Unexpected replicator type. Expected LiveSyncTrysteroReplicator."); + } return replicator; } @@ -49,7 +54,7 @@ export async function collectPeers( core: LiveSyncBaseCore, timeoutSec: number ): Promise { - const replicator = createReplicator(core); + const replicator = await createReplicator(core); await replicator.open(); try { await delay(timeoutSec * 1000); @@ -79,7 +84,7 @@ export async function syncWithPeer( peerToken: string, timeoutSec: number ): Promise { - const replicator = createReplicator(core); + const replicator = await createReplicator(core); await replicator.open(); try { const timeoutMs = timeoutSec * 1000; @@ -115,7 +120,7 @@ export async function syncWithPeer( } export async function openP2PHost(core: LiveSyncBaseCore): Promise { - const replicator = createReplicator(core); + const replicator = await createReplicator(core); await replicator.open(); return replicator; } diff --git a/src/apps/cli/main.ts b/src/apps/cli/main.ts index 7067c70..b75dd34 100644 --- a/src/apps/cli/main.ts +++ b/src/apps/cli/main.ts @@ -8,7 +8,6 @@ import * as path from "path"; import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub"; import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage"; import { LiveSyncBaseCore } from "../../LiveSyncBaseCore"; -import { ModuleReplicatorP2P } from "../../modules/core/ModuleReplicatorP2P"; import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules"; import { DEFAULT_SETTINGS, LOG_LEVEL_VERBOSE, type LOG_LEVEL, type ObsidianLiveSyncSettings } from "@lib/common/types"; import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; @@ -27,6 +26,7 @@ import type { CLICommand, CLIOptions } from "./commands/types"; import { getPathFromUXFileInfo } from "@lib/common/typeUtils"; import { stripAllPrefixes } from "@lib/string_and_binary/path"; import { IgnoreRules } from "./serviceModules/IgnoreRules"; +import { useP2PReplicatorFeature } from "@/lib/src/replication/trystero/useP2PReplicatorFeature"; const SETTINGS_FILE = ".livesync/settings.json"; ensureGlobalNodeLocalStorage(); @@ -368,12 +368,11 @@ export async function main() { (core: LiveSyncBaseCore, serviceHub: InjectableServiceHub) => { return initialiseServiceModulesCLI(vaultPath, core, serviceHub, ignoreRules, watchEnabled); }, - (core) => [ - // No modules need to be registered for P2P replication in CLI. Directly using Replicators in p2p.ts - // new ModuleReplicatorP2P(core), - ], + (core) => [], () => [], // No add-ons (core) => { + // Register P2P replicator feature. + const _replicator = useP2PReplicatorFeature(core); // Add target filter to prevent internal files are handled core.services.vault.isTargetFile.addHandler(async (target) => { const targetPath = stripAllPrefixes(getPathFromUXFileInfo(target)); @@ -424,7 +423,7 @@ export async function main() { // Save the settings file before any lifecycle events can mutate and persist them. // suspendAllSync and other lifecycle hooks clobber sync settings in memory, and // various code paths persist the clobbered state to disk. We restore on shutdown. - const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null); + const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null!); // Restore settings file on any exit to undo lifecycle mutations. // Write to a temp path first so a crash mid-write doesn't leave a truncated file. From 9d9364af36b55534700035614f74fc19c0975145 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 17 May 2026 02:24:58 +0900 Subject: [PATCH 7/7] Fix updates.md --- updates.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/updates.md b/updates.md index a5c9090..76da4db 100644 --- a/updates.md +++ b/updates.md @@ -17,12 +17,6 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid - Now `Replicate` button or ribbon icon opens a redesigned interactive replication dialogue that performs smart bidirectional sync with a single click. - The vault rebuild flow (`replicateAllFromServer`) now opens the redesigned P2P Replication modal instead of a plain text selection dialogue, providing a consistent UI experience. - -## Unreleased - -15th May, 2026 - - ## 0.25.62 14th May, 2026