From cab6679c2bf244ee07d179bac1f1ce9f9d1159c5 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Tue, 1 Sep 2026 11:33:54 +0000 Subject: [PATCH] Route interactive P2P UI through focused views --- .../P2PSync/P2PReplicator/P2PReplicationUI.ts | 20 +++--- .../P2PReplicationUI.unit.spec.ts | 20 ++---- src/main.ts | 5 +- src/serviceFeatures/useP2PReplicatorUI.ts | 38 +++------- .../useP2PReplicatorUI.unit.spec.ts | 69 ++++--------------- 5 files changed, 41 insertions(+), 111 deletions(-) diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts index f5a7859a..f94380a8 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts +++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts @@ -6,20 +6,20 @@ import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p"; 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) through the - * stable targeted-transfer view. The compatibility Replicator argument is - * intentionally unused here and remains available only to the rebuild factory. + * Create the Obsidian-owned interactive P2P entry for stable service views. + * + * Peer selection belongs to the host UI rather than the concrete compatibility + * Replicator. The returned operation opens the modal and performs bidirectional + * synchronisation, pulling before pushing, through the targeted-transfer view. * * Usage: - * const factory = createOpenReplicationUI(app); - * useP2PReplicatorFeature(core, factory); + * const createInteractiveReplication = createOpenReplicationUI(app); + * const openInteractiveReplication = createInteractiveReplication(p2p); */ export function createOpenReplicationUI( app: App -): (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise { - return (_replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => +): (p2p: P2PServiceViews) => (showResult: boolean) => Promise { + return (p2p: P2PServiceViews) => (showResult: boolean): Promise => { const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; return new Promise((resolve) => { @@ -89,7 +89,7 @@ export function createOpenReplicationUI( * * Usage: * const factory = createOpenRebuildUI(app); - * useP2PReplicatorFeature(core, createOpenReplicationUI(app), factory); + * useP2PReplicatorFeature(core, openReplicationUIFactory, factory); */ export function createOpenRebuildUI( app: App diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts index bec605b8..7f829998 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts +++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts @@ -69,7 +69,7 @@ describe("createOpenReplicationUI", () => { it("settles a cancelled peer-selection session when the modal closes", async () => { const p2p = createP2PServiceViews(); - const session = createOpenReplicationUI({} as any)(createReplicator(), p2p)(true); + const session = createOpenReplicationUI({} as any)(p2p)(true); const modal = modalState.instances[0]; expect(modal.p2p).toBe(p2p); @@ -80,9 +80,8 @@ describe("createOpenReplicationUI", () => { }); it("keeps repeated synchronisation inside the session boundary until the modal closes", async () => { - const replicator = createReplicator(); const p2p = createP2PServiceViews(); - const session = createOpenReplicationUI({} as any)(replicator, p2p)(true); + const session = createOpenReplicationUI({} as any)(p2p)(true); const modal = modalState.instances[0]; let settled = false; void session.finally(() => { @@ -102,9 +101,8 @@ describe("createOpenReplicationUI", () => { }); it("routes ordinary peer transfer through the stable targeted-transfer view", async () => { - const replicator = createReplicator(); const p2p = createP2PServiceViews(); - const session = createOpenReplicationUI({} as any)(replicator, p2p)(true); + const session = createOpenReplicationUI({} as any)(p2p)(true); const modal = modalState.instances[0]; await modal.callback.onSync("peer-a"); @@ -113,13 +111,10 @@ describe("createOpenReplicationUI", () => { expect(p2p.targetedTransfer.pullFromPeer).toHaveBeenCalledWith("peer-a", { showNotice: true }); expect(p2p.targetedTransfer.requestPushToPeer).toHaveBeenCalledWith("peer-a"); - expect(replicator.replicateFrom).not.toHaveBeenCalled(); - expect(replicator.requestSynchroniseToPeer).not.toHaveBeenCalled(); }); it("waits for an in-flight synchronisation when the modal closes", async () => { let finishPull!: (value: { status: "completed"; ok: true }) => void; - const replicator = createReplicator(); const p2p = createP2PServiceViews(); p2p.targetedTransfer.pullFromPeer.mockImplementation( async () => @@ -127,7 +122,7 @@ describe("createOpenReplicationUI", () => { finishPull = resolve; }) ); - const session = createOpenReplicationUI({} as any)(replicator, p2p)(true); + const session = createOpenReplicationUI({} as any)(p2p)(true); const modal = modalState.instances[0]; let settled = false; void session.finally(() => { @@ -146,15 +141,13 @@ describe("createOpenReplicationUI", () => { }); it("closes the P2P connection after a successful sync-and-close action", async () => { - const replicator = createReplicator(); const p2p = createP2PServiceViews(); - const session = createOpenReplicationUI({} as any)(replicator, p2p)(true); + const session = createOpenReplicationUI({} as any)(p2p)(true); const modal = modalState.instances[0]; await modal.callback.onSyncAndClose("peer-a"); expect(p2p.transportLifecycle.disconnect).toHaveBeenCalledOnce(); - expect(replicator.close).not.toHaveBeenCalled(); let settled = false; void session.finally(() => { settled = true; @@ -167,10 +160,9 @@ describe("createOpenReplicationUI", () => { }); it("returns a cancelled peer push as non-success to the presentation boundary", async () => { - const replicator = createReplicator(); const p2p = createP2PServiceViews(); p2p.targetedTransfer.requestPushToPeer.mockResolvedValue({ status: "cancelled" } as never); - const session = createOpenReplicationUI({} as any)(replicator, p2p)(true); + const session = createOpenReplicationUI({} as any)(p2p)(true); const modal = modalState.instances[0]; const actionResult = await modal.callback.onSync("peer-a"); diff --git a/src/main.ts b/src/main.ts index 5158ff32..26f2a957 100644 --- a/src/main.ts +++ b/src/main.ts @@ -178,14 +178,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin { const curriedFeature = () => featuresInitialiser(core); core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature); const setupManager = core.getModule(SetupManager); + const createInteractiveP2PReplication = createOpenReplicationUI(this.app); const replicator = useP2PReplicatorFeature( core, - createOpenReplicationUI(this.app), + (_compatibilityReplicator, p2p) => createInteractiveP2PReplication(p2p), createOpenRebuildUI(this.app) ); setupManager.registerP2PSetupConnectionProbe(replicator.connectionProbe); useP2PReplicatorCommands(core, replicator); - useP2PReplicatorUI(core, core, replicator); + useP2PReplicatorUI(core, core, replicator, createInteractiveP2PReplication(replicator)); useRemoteConfiguration(core); useSetupProtocolFeature(core, setupManager); diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index ae61ae82..afa1b112 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -1,8 +1,6 @@ import { eventHub, EVENT_REQUEST_OPEN_P2P } from "@/common/events"; -import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import { type UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult"; -import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector"; import { P2PServerStatusPaneView, VIEW_TYPE_P2P_SERVER_STATUS, @@ -21,6 +19,9 @@ class LegacyP2PStatusPaneView extends P2PServerStatusPaneView { } } +/** Host-owned peer-selection entry used by the two adjunct P2P commands. */ +export type OpenInteractiveP2PReplication = (showResult: boolean) => Promise; + export function hasP2PConfiguration(settings: Partial): boolean { if ( settings.remoteType === REMOTE_P2P || @@ -61,7 +62,8 @@ export function useP2PReplicatorUI( never >, core: LiveSyncCore, - replicator: UseP2PReplicatorResult + replicator: UseP2PReplicatorResult, + openInteractiveReplication: OpenInteractiveP2PReplication ) { const api = host.services.API as { showWindow: (type: string) => Promise; @@ -80,21 +82,6 @@ export function useP2PReplicatorUI( ) => { addClass?: (name: string) => unknown; remove?: () => void } | undefined; }; - // const env: LiveSyncTrysteroReplicatorEnv = { services: host.services as any }; - const getReplicator = () => replicator.replicator; - const p2pLogCollector = new P2PLogCollector(host.services.context.events); - const storeP2PStatusLine = reactiveSource(""); - p2pLogCollector.p2pReplicationLine.onChanged((line) => { - storeP2PStatusLine.value = line.value; - }); - const p2pParams = { - get replicator() { - return getReplicator(); - }, - p2pLogCollector, - storeP2PStatusLine, - }; - const statusFactory = (leaf: WorkspaceLeaf) => { return new P2PServerStatusPaneView(leaf, core, replicator); }; @@ -108,15 +95,11 @@ export function useP2PReplicatorUI( return api.showWindow(VIEW_TYPE_P2P_SERVER_STATUS); }; const runOpenReplication = () => { - const activeReplicator = replicator.replicator; - if (!activeReplicator) return; - const settings = host.services.setting.currentSettings(); - // The deprecated compatibility result only opens the ordinary UI; the - // actual transfer runs through the stable targeted-transfer view. - void host.services.replicator.runFiniteReplicationActivity( - () => activeReplicator.openReplication(settings, false, true, false), - { label: "replication" } - ); + // Peer selection is a host UI concern. The injected operation opens + // the dialogue, while its transfer callbacks use focused P2P views. + void host.services.replicator.runFiniteReplicationActivity(() => openInteractiveReplication(true), { + label: "replication", + }); }; // Keep the retired view type registered only long enough to restore an // existing workspace leaf with the current status UI. Layout-ready @@ -244,5 +227,4 @@ export function useP2PReplicatorUI( ); return true; }); - return p2pParams; } diff --git a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts index da4b2643..87e4cf4f 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts @@ -13,6 +13,8 @@ vi.mock("@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView", () => ({ import { useP2PReplicatorUI } from "./useP2PReplicatorUI"; +const noopOpenInteractiveReplication = () => Promise.resolve(false); + describe("useP2PReplicatorUI commands", () => { it("waits for settings to load before deciding whether to show the P2P ribbon", async () => { let initialise: (() => Promise) | undefined; @@ -49,7 +51,7 @@ describe("useP2PReplicatorUI commands", () => { }, } as any; - useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + useP2PReplicatorUI(host, {} as any, {} as any, noopOpenInteractiveReplication); await expect(initialise?.()).resolves.toBe(true); expect(currentSettings).not.toHaveBeenCalled(); @@ -64,7 +66,7 @@ describe("useP2PReplicatorUI commands", () => { it("exposes a direct modal P2P replication command as finite replication activity", async () => { const commands: Array<{ id: string; checkCallback?: (isChecking: boolean) => unknown }> = []; let initialise: (() => Promise) | undefined; - const openReplication = vi.fn(async () => true); + const openInteractiveReplication = vi.fn(async () => true); const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task()); const host = { services: { @@ -96,58 +98,18 @@ describe("useP2PReplicatorUI commands", () => { } as any; const p2p = { transportLifecycle: { isConnected: true }, - replicator: { - server: { isServing: true }, - openReplication, - replicateFromCommand: vi.fn(), - }, } as any; - useP2PReplicatorUI(host, {} as any, p2p); + useP2PReplicatorUI(host, {} as any, p2p, openInteractiveReplication); await initialise?.(); commands.find((command) => command.id === "replicate-now-by-p2p")?.checkCallback?.(false); - await vi.waitFor(() => expect(openReplication).toHaveBeenCalledOnce()); + await vi.waitFor(() => expect(openInteractiveReplication).toHaveBeenCalledWith(true)); expect(runFiniteReplicationActivity).toHaveBeenCalledWith(expect.any(Function), { label: "replication", }); }); - it("keeps the current replicator in the pane parameters after replacement", () => { - const first = { id: "first" }; - const second = { id: "second" }; - let current = first; - const p2p = { - get replicator() { - return current; - }, - } as any; - const host = { - services: { - context: createServiceContext(), - API: { - showWindow: vi.fn(async () => undefined), - registerWindow: vi.fn(), - addCommand: vi.fn(), - addRibbonIcon: vi.fn(), - getPlatform: vi.fn(() => "obsidian"), - }, - appLifecycle: { - onInitialise: { addHandler: vi.fn() }, - onSettingLoaded: { addHandler: vi.fn() }, - onLayoutReady: { addHandler: vi.fn() }, - }, - setting: { currentSettings: vi.fn(() => ({ remoteType: "COUCHDB" })) }, - replicator: { runFiniteReplicationActivity: vi.fn() }, - }, - } as any; - - const paneParams = useP2PReplicatorUI(host, {} as any, p2p); - current = second; - - expect(paneParams.replicator).toBe(second); - }); - it("retains only the current P2P status command and routes existing open requests to it", async () => { const commands: Array<{ id: string; @@ -186,9 +148,9 @@ describe("useP2PReplicatorUI commands", () => { replicator: { runFiniteReplicationActivity: vi.fn() }, }, } as any; - const p2p = { replicator: undefined } as any; + const p2p = {} as any; - useP2PReplicatorUI(host, {} as any, p2p); + useP2PReplicatorUI(host, {} as any, p2p, noopOpenInteractiveReplication); await initialise?.(); expect(commands.map((command) => command.id)).not.toContain("open-p2p-replicator"); @@ -237,19 +199,13 @@ describe("useP2PReplicatorUI commands", () => { replicator: { runFiniteReplicationActivity }, }, } as any; - const replicateFromCommand = vi.fn(); const synchroniseConfiguredTargets = vi.fn(async () => ({ status: "completed" })); const p2p = { transportLifecycle: { isConnected: true }, targetedTransfer: { synchroniseConfiguredTargets }, - replicator: { - server: { isServing: true }, - openReplication: vi.fn(), - replicateFromCommand, - }, } as any; - useP2PReplicatorUI(host, {} as any, p2p); + useP2PReplicatorUI(host, {} as any, p2p, noopOpenInteractiveReplication); await initialise?.(); for (const commandId of [ @@ -283,7 +239,6 @@ describe("useP2PReplicatorUI commands", () => { commands.find(({ id }) => id === "p2p-sync-targets")?.checkCallback?.(false); await vi.waitFor(() => expect(synchroniseConfiguredTargets).toHaveBeenCalledOnce()); - expect(replicateFromCommand).not.toHaveBeenCalled(); expect(runFiniteReplicationActivity).toHaveBeenCalledWith(expect.any(Function), { label: "replication", }); @@ -323,7 +278,7 @@ describe("useP2PReplicatorUI commands", () => { }, } as any; - useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + useP2PReplicatorUI(host, {} as any, {} as any, noopOpenInteractiveReplication); await layoutReady?.(); expect(showWindow).not.toHaveBeenCalled(); @@ -379,7 +334,7 @@ describe("useP2PReplicatorUI commands", () => { }, } as any; - useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + useP2PReplicatorUI(host, {} as any, {} as any, noopOpenInteractiveReplication); await initialise?.(); await settingLoaded?.(); expect(addRibbonIcon).not.toHaveBeenCalled(); @@ -453,7 +408,7 @@ describe("useP2PReplicatorUI commands", () => { }, } as any; - useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + useP2PReplicatorUI(host, {} as any, {} as any, noopOpenInteractiveReplication); await layoutReady?.(); expect(legacyLeaf.setViewState).toHaveBeenCalledWith({