mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-27 05:47:07 +00:00
Refine P2P and manual setup workflows
This commit is contained in:
@@ -93,7 +93,7 @@ export async function askSimpleFetchMode(
|
||||
|
||||
const msg = `We are about to retrieve the remote data.
|
||||
|
||||
Firstly, how shall we handle the data retrieved from this remote server?
|
||||
Firstly, how shall we handle the data retrieved from this remote source?
|
||||
|
||||
- **${SIMPLE_FETCH_STAGE1_NEWER_WINS}**: Compares the modified time of files and takes the newer one.
|
||||
If you have been using Self-hosted LiveSync and have made changes on multiple devices, this option may be suitable for you as it tries to merge changes based on modified time.
|
||||
|
||||
@@ -469,6 +469,9 @@ describe("Red Flag Feature", () => {
|
||||
expect(result).toBe(true);
|
||||
expect(host.mocks.rebuilder.$fetchLocalDBFast).toHaveBeenCalled();
|
||||
expect(synchroniseAllFilesBetweenDBandStorage).toHaveBeenCalled();
|
||||
const firstPrompt = host.mocks.ui.confirm.confirmWithMessage.mock.calls[0]?.[1];
|
||||
expect(firstPrompt).toContain("data retrieved from this remote source");
|
||||
expect(firstPrompt).not.toContain("remote server");
|
||||
// We can't easily check performFullScan call here because it's imported,
|
||||
// but we can verify rebuilder was called.
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ 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 { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView";
|
||||
import {
|
||||
P2PServerStatusPaneView,
|
||||
VIEW_TYPE_P2P_SERVER_STATUS,
|
||||
@@ -11,6 +10,34 @@ import {
|
||||
import type { LiveSyncCore } from "@/main";
|
||||
import type { WorkspaceLeaf } from "@/deps";
|
||||
import { REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
|
||||
export const LEGACY_VIEW_TYPE_P2P = "p2p-replicator";
|
||||
|
||||
class LegacyP2PStatusPaneView extends P2PServerStatusPaneView {
|
||||
override getViewType() {
|
||||
return LEGACY_VIEW_TYPE_P2P;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasP2PConfiguration(settings: Partial<ObsidianLiveSyncSettings>): boolean {
|
||||
if (
|
||||
settings.remoteType === REMOTE_P2P ||
|
||||
settings.P2P_Enabled === true ||
|
||||
(settings.P2P_roomID ?? "").trim() !== "" ||
|
||||
(settings.P2P_passphrase ?? "").trim() !== ""
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return Object.values(settings.remoteConfigurations ?? {}).some((configuration) => {
|
||||
try {
|
||||
return ConnectionStringParser.parse(configuration.uri).type === "p2p";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsidian-specific P2P views, commands, status collection, and ribbon wiring.
|
||||
@@ -45,8 +72,7 @@ export function useP2PReplicatorUI(
|
||||
icon: string,
|
||||
title: string,
|
||||
callback: () => void
|
||||
) => { addClass?: (name: string) => unknown } | undefined;
|
||||
getPlatform: () => string;
|
||||
) => { addClass?: (name: string) => unknown; remove?: () => void } | undefined;
|
||||
};
|
||||
|
||||
// const env: LiveSyncTrysteroReplicatorEnv = { services: host.services as any };
|
||||
@@ -64,15 +90,12 @@ export function useP2PReplicatorUI(
|
||||
storeP2PStatusLine,
|
||||
};
|
||||
|
||||
// Register view, commands and ribbon if a view factory is provided
|
||||
const viewType = VIEW_TYPE_P2P;
|
||||
const factory = (leaf: WorkspaceLeaf) => {
|
||||
return new P2PReplicatorPaneView(leaf, core, p2pParams);
|
||||
};
|
||||
const statusFactory = (leaf: WorkspaceLeaf) => {
|
||||
return new P2PServerStatusPaneView(leaf, core, p2pParams);
|
||||
};
|
||||
const openPane = () => api.showWindow(viewType);
|
||||
const legacyStatusFactory = (leaf: WorkspaceLeaf) => {
|
||||
return new LegacyP2PStatusPaneView(leaf, core, p2pParams);
|
||||
};
|
||||
const openStatusPane = () => {
|
||||
if (api.showWindowOnRight) {
|
||||
return api.showWindowOnRight(VIEW_TYPE_P2P_SERVER_STATUS);
|
||||
@@ -88,20 +111,36 @@ export function useP2PReplicatorUI(
|
||||
{ label: "replication" }
|
||||
);
|
||||
};
|
||||
api.registerWindow(viewType, factory);
|
||||
// Keep the retired view type registered only long enough to restore an
|
||||
// existing workspace leaf with the current status UI. Layout-ready
|
||||
// migration below rewrites it to the current type without opening a leaf.
|
||||
api.registerWindow(LEGACY_VIEW_TYPE_P2P, legacyStatusFactory);
|
||||
api.registerWindow(VIEW_TYPE_P2P_SERVER_STATUS, statusFactory);
|
||||
|
||||
let ribbonElement: { addClass?: (name: string) => unknown; remove?: () => void } | undefined;
|
||||
const updateRibbon = (settings: Partial<ObsidianLiveSyncSettings>) => {
|
||||
if (hasP2PConfiguration(settings)) {
|
||||
if (ribbonElement) return;
|
||||
ribbonElement = api.addRibbonIcon("waypoints", "P2P Status", () => {
|
||||
void openStatusPane();
|
||||
});
|
||||
ribbonElement?.addClass?.("livesync-ribbon-p2p-server-status");
|
||||
return;
|
||||
}
|
||||
ribbonElement?.remove?.();
|
||||
ribbonElement = undefined;
|
||||
};
|
||||
|
||||
// Settings are loaded after onInitialise. Reading them from the earlier
|
||||
// phase aborts the plug-in lifecycle before the local database can open.
|
||||
host.services.appLifecycle.onSettingLoaded.addHandler(() => {
|
||||
updateRibbon(host.services.setting.currentSettings());
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
|
||||
host.services.appLifecycle.onInitialise.addHandler(() => {
|
||||
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => {
|
||||
void openPane();
|
||||
});
|
||||
|
||||
api.addCommand({
|
||||
id: "open-p2p-replicator",
|
||||
name: "P2P Sync : Open P2P Replicator (Old UI)",
|
||||
callback: () => {
|
||||
void openPane();
|
||||
},
|
||||
void openStatusPane();
|
||||
});
|
||||
|
||||
api.addCommand({
|
||||
@@ -147,27 +186,37 @@ export function useP2PReplicatorUI(
|
||||
},
|
||||
});
|
||||
|
||||
// api.addRibbonIcon("waypoints", "P2P Replicator", () => {
|
||||
// void openPane();
|
||||
// })?.addClass?.("livesync-ribbon-replicate-p2p");
|
||||
|
||||
api.addRibbonIcon("waypoints", "P2P Status", () => {
|
||||
void openStatusPane();
|
||||
})?.addClass?.("livesync-ribbon-p2p-server-status");
|
||||
host.services.setting.onSettingSaved?.addHandler((settings) => {
|
||||
updateRibbon(settings);
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
|
||||
host.services.appLifecycle.onLayoutReady.addHandler(() => {
|
||||
if (api.getPlatform() !== "obsidian") {
|
||||
return Promise.resolve(true);
|
||||
host.services.appLifecycle.onLayoutReady.addHandler(async () => {
|
||||
const workspace = (
|
||||
host.services.context as {
|
||||
app?: {
|
||||
workspace?: {
|
||||
getLeavesOfType(type: string): WorkspaceLeaf[];
|
||||
};
|
||||
};
|
||||
}
|
||||
).app?.workspace;
|
||||
if (!workspace) {
|
||||
return 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);
|
||||
const legacyLeaves = workspace.getLeavesOfType(LEGACY_VIEW_TYPE_P2P);
|
||||
await Promise.all(
|
||||
legacyLeaves.map((leaf) =>
|
||||
leaf.setViewState({
|
||||
type: VIEW_TYPE_P2P_SERVER_STATUS,
|
||||
active: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
return true;
|
||||
});
|
||||
return p2pParams;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,66 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { eventHub, EVENT_REQUEST_OPEN_P2P } from "@/common/events";
|
||||
|
||||
vi.mock("@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView", () => ({
|
||||
P2PReplicatorPaneView: class {},
|
||||
VIEW_TYPE_P2P: "p2p",
|
||||
}));
|
||||
vi.mock("@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView", () => ({
|
||||
P2PServerStatusPaneView: class {},
|
||||
P2PServerStatusPaneView: class {
|
||||
getViewType() {
|
||||
return "p2p-status";
|
||||
}
|
||||
},
|
||||
VIEW_TYPE_P2P_SERVER_STATUS: "p2p-status",
|
||||
}));
|
||||
|
||||
import { useP2PReplicatorUI } from "./useP2PReplicatorUI";
|
||||
|
||||
describe("useP2PReplicatorUI commands", () => {
|
||||
it("waits for settings to load before deciding whether to show the P2P ribbon", async () => {
|
||||
let initialise: (() => Promise<unknown>) | undefined;
|
||||
let settingLoaded: (() => Promise<unknown>) | undefined;
|
||||
let settings: Record<string, unknown> | undefined;
|
||||
const currentSettings = vi.fn(() => settings);
|
||||
const host = {
|
||||
services: {
|
||||
context: createServiceContext(),
|
||||
API: {
|
||||
showWindow: vi.fn(async () => undefined),
|
||||
registerWindow: vi.fn(),
|
||||
addCommand: vi.fn(),
|
||||
addRibbonIcon: vi.fn(),
|
||||
},
|
||||
appLifecycle: {
|
||||
onInitialise: {
|
||||
addHandler: vi.fn((handler) => {
|
||||
initialise = handler;
|
||||
}),
|
||||
},
|
||||
onSettingLoaded: {
|
||||
addHandler: vi.fn((handler) => {
|
||||
settingLoaded = handler;
|
||||
}),
|
||||
},
|
||||
onLayoutReady: { addHandler: vi.fn() },
|
||||
},
|
||||
setting: {
|
||||
currentSettings,
|
||||
onSettingSaved: { addHandler: vi.fn() },
|
||||
},
|
||||
replicator: { runFiniteReplicationActivity: vi.fn() },
|
||||
},
|
||||
} as any;
|
||||
|
||||
useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any);
|
||||
|
||||
await expect(initialise?.()).resolves.toBe(true);
|
||||
expect(currentSettings).not.toHaveBeenCalled();
|
||||
settings = {
|
||||
remoteType: "COUCHDB",
|
||||
remoteConfigurations: {},
|
||||
};
|
||||
await expect(settingLoaded?.()).resolves.toBe(true);
|
||||
expect(currentSettings).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
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<unknown>) | undefined;
|
||||
@@ -34,6 +82,7 @@ describe("useP2PReplicatorUI commands", () => {
|
||||
initialise = handler;
|
||||
}),
|
||||
},
|
||||
onSettingLoaded: { addHandler: vi.fn() },
|
||||
onLayoutReady: { addHandler: vi.fn() },
|
||||
},
|
||||
setting: { currentSettings: vi.fn(() => ({ remoteType: "COUCHDB" })) },
|
||||
@@ -79,6 +128,7 @@ describe("useP2PReplicatorUI commands", () => {
|
||||
},
|
||||
appLifecycle: {
|
||||
onInitialise: { addHandler: vi.fn() },
|
||||
onSettingLoaded: { addHandler: vi.fn() },
|
||||
onLayoutReady: { addHandler: vi.fn() },
|
||||
},
|
||||
setting: { currentSettings: vi.fn(() => ({ remoteType: "COUCHDB" })) },
|
||||
@@ -91,4 +141,226 @@ describe("useP2PReplicatorUI commands", () => {
|
||||
|
||||
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; callback?: () => void }> = [];
|
||||
let initialise: (() => Promise<unknown>) | undefined;
|
||||
const showWindow = vi.fn(async () => undefined);
|
||||
const showWindowOnRight = vi.fn(async () => undefined);
|
||||
const host = {
|
||||
services: {
|
||||
context: createServiceContext(),
|
||||
API: {
|
||||
showWindow,
|
||||
showWindowOnRight,
|
||||
registerWindow: vi.fn(),
|
||||
addCommand: vi.fn((command) => commands.push(command)),
|
||||
addRibbonIcon: vi.fn(),
|
||||
getPlatform: vi.fn(() => "desktop"),
|
||||
},
|
||||
appLifecycle: {
|
||||
onInitialise: {
|
||||
addHandler: vi.fn((handler) => {
|
||||
initialise = handler;
|
||||
}),
|
||||
},
|
||||
onSettingLoaded: { addHandler: vi.fn() },
|
||||
onLayoutReady: { addHandler: vi.fn() },
|
||||
},
|
||||
setting: {
|
||||
currentSettings: vi.fn(() => ({
|
||||
remoteType: "COUCHDB",
|
||||
remoteConfigurations: {},
|
||||
})),
|
||||
},
|
||||
replicator: { runFiniteReplicationActivity: vi.fn() },
|
||||
},
|
||||
} as any;
|
||||
const p2p = { replicator: undefined } as any;
|
||||
|
||||
useP2PReplicatorUI(host, {} as any, p2p);
|
||||
await initialise?.();
|
||||
|
||||
expect(commands.map((command) => command.id)).not.toContain("open-p2p-replicator");
|
||||
expect(commands.map((command) => command.id)).toContain("open-p2p-server-status");
|
||||
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P);
|
||||
await vi.waitFor(() => expect(showWindowOnRight).toHaveBeenCalledWith("p2p-status"));
|
||||
expect(showWindow).not.toHaveBeenCalledWith("p2p");
|
||||
});
|
||||
|
||||
it("does not open the P2P status pane automatically when the workspace becomes ready", async () => {
|
||||
let layoutReady: (() => Promise<unknown>) | undefined;
|
||||
const showWindow = vi.fn(async () => undefined);
|
||||
const showWindowOnRight = vi.fn(async () => undefined);
|
||||
const host = {
|
||||
services: {
|
||||
context: createServiceContext(),
|
||||
API: {
|
||||
showWindow,
|
||||
showWindowOnRight,
|
||||
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((handler) => {
|
||||
layoutReady = handler;
|
||||
}),
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
currentSettings: vi.fn(() => ({
|
||||
remoteType: "COUCHDB",
|
||||
remoteConfigurations: {},
|
||||
})),
|
||||
},
|
||||
replicator: { runFiniteReplicationActivity: vi.fn() },
|
||||
},
|
||||
} as any;
|
||||
|
||||
useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any);
|
||||
await layoutReady?.();
|
||||
|
||||
expect(showWindow).not.toHaveBeenCalled();
|
||||
expect(showWindowOnRight).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows the ribbon only whilst a P2P configuration exists", async () => {
|
||||
let initialise: (() => Promise<unknown>) | undefined;
|
||||
let settingLoaded: (() => Promise<unknown>) | undefined;
|
||||
let onSettingSaved: ((settings: unknown) => Promise<unknown>) | undefined;
|
||||
let currentSettings: any = {
|
||||
remoteType: "COUCHDB",
|
||||
remoteConfigurations: {},
|
||||
P2P_Enabled: false,
|
||||
P2P_roomID: "",
|
||||
P2P_passphrase: "",
|
||||
};
|
||||
const ribbon = { addClass: vi.fn(), remove: vi.fn() };
|
||||
const addRibbonIcon = vi.fn(() => ribbon);
|
||||
const host = {
|
||||
services: {
|
||||
context: createServiceContext(),
|
||||
API: {
|
||||
showWindow: vi.fn(async () => undefined),
|
||||
showWindowOnRight: vi.fn(async () => undefined),
|
||||
registerWindow: vi.fn(),
|
||||
addCommand: vi.fn(),
|
||||
addRibbonIcon,
|
||||
getPlatform: vi.fn(() => "desktop"),
|
||||
},
|
||||
appLifecycle: {
|
||||
onInitialise: {
|
||||
addHandler: vi.fn((handler) => {
|
||||
initialise = handler;
|
||||
}),
|
||||
},
|
||||
onSettingLoaded: {
|
||||
addHandler: vi.fn((handler) => {
|
||||
settingLoaded = handler;
|
||||
}),
|
||||
},
|
||||
onLayoutReady: { addHandler: vi.fn() },
|
||||
},
|
||||
setting: {
|
||||
currentSettings: vi.fn(() => currentSettings),
|
||||
onSettingSaved: {
|
||||
addHandler: vi.fn((handler) => {
|
||||
onSettingSaved = handler;
|
||||
}),
|
||||
},
|
||||
},
|
||||
replicator: { runFiniteReplicationActivity: vi.fn() },
|
||||
},
|
||||
} as any;
|
||||
|
||||
useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any);
|
||||
await initialise?.();
|
||||
await settingLoaded?.();
|
||||
expect(addRibbonIcon).not.toHaveBeenCalled();
|
||||
|
||||
currentSettings = {
|
||||
...currentSettings,
|
||||
remoteConfigurations: {
|
||||
peer: {
|
||||
id: "peer",
|
||||
name: "Peer",
|
||||
uri: "sls+p2p://room?passphrase=secret",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
await onSettingSaved?.(currentSettings);
|
||||
expect(addRibbonIcon).toHaveBeenCalledOnce();
|
||||
|
||||
await onSettingSaved?.(currentSettings);
|
||||
expect(addRibbonIcon).toHaveBeenCalledOnce();
|
||||
|
||||
currentSettings = {
|
||||
...currentSettings,
|
||||
remoteConfigurations: {},
|
||||
};
|
||||
await onSettingSaved?.(currentSettings);
|
||||
expect(ribbon.remove).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("replaces a restored legacy P2P leaf with the current status view without opening another leaf", async () => {
|
||||
let layoutReady: (() => Promise<unknown>) | undefined;
|
||||
const legacyLeaf = {
|
||||
setViewState: vi.fn(async () => undefined),
|
||||
};
|
||||
const workspace = {
|
||||
getLeavesOfType: vi.fn((type: string) => (type === "p2p-replicator" ? [legacyLeaf] : [])),
|
||||
};
|
||||
const context = createServiceContext() as ReturnType<typeof createServiceContext> & {
|
||||
app: { workspace: typeof workspace };
|
||||
};
|
||||
context.app = { workspace };
|
||||
const showWindow = vi.fn(async () => undefined);
|
||||
const showWindowOnRight = vi.fn(async () => undefined);
|
||||
const host = {
|
||||
services: {
|
||||
context,
|
||||
API: {
|
||||
showWindow,
|
||||
showWindowOnRight,
|
||||
registerWindow: vi.fn(),
|
||||
addCommand: vi.fn(),
|
||||
addRibbonIcon: vi.fn(),
|
||||
getPlatform: vi.fn(() => "desktop"),
|
||||
},
|
||||
appLifecycle: {
|
||||
onInitialise: { addHandler: vi.fn() },
|
||||
onSettingLoaded: { addHandler: vi.fn() },
|
||||
onLayoutReady: {
|
||||
addHandler: vi.fn((handler) => {
|
||||
layoutReady = handler;
|
||||
}),
|
||||
},
|
||||
},
|
||||
setting: {
|
||||
currentSettings: vi.fn(() => ({
|
||||
remoteType: "COUCHDB",
|
||||
remoteConfigurations: {},
|
||||
})),
|
||||
},
|
||||
replicator: { runFiniteReplicationActivity: vi.fn() },
|
||||
},
|
||||
} as any;
|
||||
|
||||
useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any);
|
||||
await layoutReady?.();
|
||||
|
||||
expect(legacyLeaf.setViewState).toHaveBeenCalledWith({
|
||||
type: "p2p-status",
|
||||
active: false,
|
||||
});
|
||||
expect(showWindow).not.toHaveBeenCalled();
|
||||
expect(showWindowOnRight).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user