Enhance P2P synchronization features and UI improvements

This commit is contained in:
vorotamoroz
2026-05-17 01:36:09 +09:00
parent 9a90256a8a
commit 4ed1749652
8 changed files with 145 additions and 32 deletions
@@ -13,17 +13,26 @@ export class P2POpenReplicationModal extends Modal {
callback?: P2POpenReplicationModalCallback; callback?: P2POpenReplicationModalCallback;
component?: ReturnType<typeof mount>; component?: ReturnType<typeof mount>;
showResult: boolean; showResult: boolean;
title: string;
onClosed?: () => void;
rebuildMode: boolean;
constructor( constructor(
app: App, app: App,
liveSyncReplicator: LiveSyncTrysteroReplicator, liveSyncReplicator: LiveSyncTrysteroReplicator,
callback?: P2POpenReplicationModalCallback, callback?: P2POpenReplicationModalCallback,
showResult: boolean = false showResult: boolean = false,
title: string = "P2P Replication",
onClosed?: () => void,
rebuildMode: boolean = false
) { ) {
super(app); super(app);
this.liveSyncReplicator = liveSyncReplicator; this.liveSyncReplicator = liveSyncReplicator;
this.callback = callback; this.callback = callback;
this.showResult = showResult; this.showResult = showResult;
this.title = title;
this.onClosed = onClosed;
this.rebuildMode = rebuildMode;
} }
async onSync(peerId: string) { async onSync(peerId: string) {
@@ -41,7 +50,7 @@ export class P2POpenReplicationModal extends Modal {
override onOpen() { override onOpen() {
const { contentEl } = this; const { contentEl } = this;
this.titleEl.setText("P2P Replication"); this.titleEl.setText(this.title);
contentEl.empty(); contentEl.empty();
if (this.component === undefined) { if (this.component === undefined) {
@@ -53,6 +62,7 @@ export class P2POpenReplicationModal extends Modal {
onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId), onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId),
onClose: () => this.close(), onClose: () => this.close(),
showResult: this.showResult, showResult: this.showResult,
rebuildMode: this.rebuildMode,
}, },
}); });
} }
@@ -65,5 +75,6 @@ export class P2POpenReplicationModal extends Modal {
void unmount(this.component); void unmount(this.component);
this.component = undefined; this.component = undefined;
} }
this.onClosed?.();
} }
} }
@@ -19,9 +19,10 @@
onSyncAndClose: (_peerId: string) => Promise<void>; onSyncAndClose: (_peerId: string) => Promise<void>;
onClose: () => void; onClose: () => void;
showResult: boolean; 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<P2PServerInfo | undefined>(undefined); let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let syncingPeerId = $state<string | null>(null); let syncingPeerId = $state<string | null>(null);
@@ -36,10 +37,10 @@
const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => { const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
serverInfo = status; serverInfo = status;
}); });
fireAndForget(async ()=>{ fireAndForget(async () => {
await delay(100); await delay(100);
await requestServerStatus(); await requestServerStatus();
}) });
return unsubscribe; return unsubscribe;
}); });
@@ -55,6 +56,18 @@
syncingPeerId = null; 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) { async function handleSyncAndClose(peerId: string) {
fireAndForget(async () => { fireAndForget(async () => {
@@ -68,7 +81,7 @@
}); });
onClose(); onClose();
} }
async function disconnect(){ async function disconnect() {
try { try {
await liveSyncReplicator.close(); await liveSyncReplicator.close();
Logger("Signalling connection closed.", logLevel); Logger("Signalling connection closed.", logLevel);
@@ -76,7 +89,7 @@
Logger(`Failed to close signalling connection: ${e instanceof Error ? e.message : String(e)}`, logLevel); Logger(`Failed to close signalling connection: ${e instanceof Error ? e.message : String(e)}`, logLevel);
} }
} }
async function onCloseAndDisconnect(){ async function onCloseAndDisconnect() {
await disconnect(); await disconnect();
onClose(); onClose();
} }
@@ -97,10 +110,7 @@
</script> </script>
<div class="p2p-container"> <div class="p2p-container">
<P2PServerStatusCard <P2PServerStatusCard {liveSyncReplicator} showBroadcastToggle={false} />
{liveSyncReplicator}
showBroadcastToggle={false}
/>
<div class="peers-section"> <div class="peers-section">
<h3>Available Peers</h3> <h3>Available Peers</h3>
@@ -121,20 +131,30 @@
</div> </div>
</div> </div>
<div class="peer-actions"> <div class="peer-actions">
<button {#if !rebuildMode}
class="btn btn-primary" <button
disabled={syncingPeerId !== null} class="btn btn-primary"
onclick={() => handleSync(peer.peerId)} disabled={syncingPeerId !== null}
> onclick={() => handleSync(peer.peerId)}
{syncingPeerId === peer.peerId ? "Syncing..." : "Sync"} >
</button> {syncingPeerId === peer.peerId ? "Syncing..." : "Sync"}
<button </button>
class="btn btn-secondary" <button
disabled={syncingPeerId !== null} class="btn {rebuildMode ? 'btn-primary' : 'btn-secondary'}"
onclick={() => handleSyncAndClose(peer.peerId)} disabled={syncingPeerId !== null}
> onclick={() => handleSyncAndClose(peer.peerId)}
Start Sync &amp; Close >
</button> {syncingPeerId === peer.peerId ? "Syncing..." : "Start Sync & Close"}
</button>
{:else}
<button
class="btn {rebuildMode ? 'btn-primary' : 'btn-secondary'}"
disabled={syncingPeerId !== null}
onclick={() => handleSyncThenClose(peer.peerId)}
>
{syncingPeerId === peer.peerId ? "Syncing..." : "Sync"}
</button>
{/if}
</div> </div>
</div> </div>
{/each} {/each}
@@ -145,8 +165,12 @@
</div> </div>
<div class="footer"> <div class="footer">
<button class="btn btn-cancel" onclick={onClose}>Close</button> {#if rebuildMode}
<button class="btn btn-cancel" onclick={onCloseAndDisconnect}>Close & Disconnect</button> <button class="btn btn-cancel" onclick={onClose} disabled={syncingPeerId !== null}>Skip and close</button>
{:else}
<button class="btn btn-cancel" onclick={onClose}>Close</button>
<button class="btn btn-cancel" onclick={onCloseAndDisconnect}>Close & Disconnect</button>
{/if}
</div> </div>
</div> </div>
@@ -1,7 +1,7 @@
import { App } from "@/deps.ts"; import { App } from "@/deps.ts";
import { Logger } from "@lib/common/logger"; import { Logger } from "@lib/common/logger";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types"; 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"; 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<boolean | void> {
return (replicator: LiveSyncTrysteroReplicator) =>
(showResult: boolean): Promise<boolean | void> => {
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
return new Promise<boolean | void>((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();
});
};
}
+1 -1
Submodule src/lib updated: 4ce9136a43...cc552acad2
+6 -2
View File
@@ -44,7 +44,7 @@ import { useSetupManagerHandlersFeature } from "./serviceFeatures/setupObsidian/
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature.ts"; import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature.ts";
import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands.ts"; import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands.ts";
import { useP2PReplicatorUI } from "./serviceFeatures/useP2PReplicatorUI.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<ObsidianServiceContext, LiveSyncCommands>; export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
export default class ObsidianLiveSyncPlugin extends Plugin { export default class ObsidianLiveSyncPlugin extends Plugin {
core: LiveSyncCore; core: LiveSyncCore;
@@ -177,7 +177,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const curriedFeature = () => featuresInitialiser(core); const curriedFeature = () => featuresInitialiser(core);
core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature); core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
const setupManager = core.getModule(SetupManager); 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); useP2PReplicatorCommands(core, replicator);
useP2PReplicatorUI(core, core, replicator); useP2PReplicatorUI(core, core, replicator);
useRemoteConfiguration(core); useRemoteConfiguration(core);
@@ -14,6 +14,7 @@ import { AbstractModule } from "../AbstractModule.ts";
import { $msg } from "../../lib/src/common/i18n.ts"; import { $msg } from "../../lib/src/common/i18n.ts";
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts"; import type { LiveSyncCore } from "../../main.ts";
import { REMOTE_P2P } from "@lib/common/models/setting.const.ts";
export class ModuleResolvingMismatchedTweaks extends AbstractModule { export class ModuleResolvingMismatchedTweaks extends AbstractModule {
async _anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> { async _anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
@@ -186,6 +187,9 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
async _checkAndAskUseRemoteConfiguration( async _checkAndAskUseRemoteConfiguration(
trialSetting: RemoteDBSettings trialSetting: RemoteDBSettings
): Promise<{ result: false | TweakValues; requireFetch: boolean }> { ): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
if (trialSetting.remoteType === REMOTE_P2P) {
return { result: false, requireFetch: false };
}
const preferred = await this.services.tweakValue.fetchRemotePreferred(trialSetting); const preferred = await this.services.tweakValue.fetchRemotePreferred(trialSetting);
if (preferred) { if (preferred) {
return await this.services.tweakValue.askUseRemoteConfiguration(trialSetting, preferred); return await this.services.tweakValue.askUseRemoteConfiguration(trialSetting, preferred);
@@ -39,6 +39,11 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
} }
override async showWindowOnRight(viewType: string): Promise<void> { override async showWindowOnRight(viewType: string): Promise<void> {
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); const rightLeaf = this.app.workspace.getRightLeaf(false);
if (rightLeaf) { if (rightLeaf) {
await rightLeaf.setViewState({ await rightLeaf.setViewState({
+8 -1
View File
@@ -5,7 +5,7 @@ import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/re
import FetchEverything from "../modules/features/SetupWizard/dialogs/FetchEverything.svelte"; import FetchEverything from "../modules/features/SetupWizard/dialogs/FetchEverything.svelte";
import RebuildEverything from "../modules/features/SetupWizard/dialogs/RebuildEverything.svelte"; import RebuildEverything from "../modules/features/SetupWizard/dialogs/RebuildEverything.svelte";
import { extractObject } from "octagonal-wheels/object"; 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 type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
import { TweakValuesShouldMatchedTemplate } from "@lib/common/models/tweak.definition"; import { TweakValuesShouldMatchedTemplate } from "@lib/common/models/tweak.definition";
@@ -200,6 +200,13 @@ export async function adjustSettingToRemoteIfNeeded(
return; 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. // Remote configuration fetched and applied.
if (await adjustSettingToRemote(host, log, config)) { if (await adjustSettingToRemote(host, log, config)) {
config = host.services.setting.currentSettings(); config = host.services.setting.currentSettings();