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

View File

@@ -13,17 +13,26 @@ export class P2POpenReplicationModal extends Modal {
callback?: P2POpenReplicationModalCallback;
component?: ReturnType<typeof mount>;
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?.();
}
}

View File

@@ -19,9 +19,10 @@
onSyncAndClose: (_peerId: string) => Promise<void>;
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<P2PServerInfo | undefined>(undefined);
let syncingPeerId = $state<string | null>(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 @@
</script>
<div class="p2p-container">
<P2PServerStatusCard
{liveSyncReplicator}
showBroadcastToggle={false}
/>
<P2PServerStatusCard {liveSyncReplicator} showBroadcastToggle={false} />
<div class="peers-section">
<h3>Available Peers</h3>
@@ -121,20 +131,30 @@
</div>
</div>
<div class="peer-actions">
<button
class="btn btn-primary"
disabled={syncingPeerId !== null}
onclick={() => handleSync(peer.peerId)}
>
{syncingPeerId === peer.peerId ? "Syncing..." : "Sync"}
</button>
<button
class="btn btn-secondary"
disabled={syncingPeerId !== null}
onclick={() => handleSyncAndClose(peer.peerId)}
>
Start Sync &amp; Close
</button>
{#if !rebuildMode}
<button
class="btn btn-primary"
disabled={syncingPeerId !== null}
onclick={() => handleSync(peer.peerId)}
>
{syncingPeerId === peer.peerId ? "Syncing..." : "Sync"}
</button>
<button
class="btn {rebuildMode ? 'btn-primary' : 'btn-secondary'}"
disabled={syncingPeerId !== null}
onclick={() => handleSyncAndClose(peer.peerId)}
>
{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>
{/each}
@@ -145,8 +165,12 @@
</div>
<div class="footer">
<button class="btn btn-cancel" onclick={onClose}>Close</button>
<button class="btn btn-cancel" onclick={onCloseAndDisconnect}>Close & Disconnect</button>
{#if rebuildMode}
<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>

View File

@@ -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<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();
});
};
}

Submodule src/lib updated: 4ce9136a43...cc552acad2

View File

@@ -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<ObsidianServiceContext, LiveSyncCommands>;
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);

View File

@@ -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<boolean | "CHECKAGAIN" | undefined> {
@@ -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);

View File

@@ -39,6 +39,11 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
}
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);
if (rightLeaf) {
await rightLeaf.setViewState({

View File

@@ -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();