From 4ed1749652c5534409e8016a32310bf0ea29224c Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 17 May 2026 01:36:09 +0900 Subject: [PATCH] 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();