mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-10 19:32:29 +00:00
Route P2P Setup probes through the room owner
This commit is contained in:
@@ -9791,6 +9791,10 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
|||||||
zh: "仅供测试 - 通过同步文件的较新副本来解决文件冲突,这可能会覆盖修改过的文件。请注意 ",
|
zh: "仅供测试 - 通过同步文件的较新副本来解决文件冲突,这可能会覆盖修改过的文件。请注意 ",
|
||||||
"zh-tw": "僅供測試 —— 透過同步較新的檔案版本解決衝突,這可能會覆寫已修改的檔案,請注意。",
|
"zh-tw": "僅供測試 —— 透過同步較新的檔案版本解決衝突,這可能會覆寫已修改的檔案,請注意。",
|
||||||
},
|
},
|
||||||
|
"The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.":
|
||||||
|
{
|
||||||
|
def: "The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.",
|
||||||
|
},
|
||||||
"The connection to the server has been configured successfully. As the next step,": {
|
"The connection to the server has been configured successfully. As the next step,": {
|
||||||
def: "The connection to the server has been configured successfully. As the next step,",
|
def: "The connection to the server has been configured successfully. As the next step,",
|
||||||
es: "La conexión con el servidor se ha configurado correctamente. Como paso siguiente,",
|
es: "La conexión con el servidor se ha configurado correctamente. Como paso siguiente,",
|
||||||
|
|||||||
@@ -1062,6 +1062,7 @@
|
|||||||
"Target patterns": "Target patterns",
|
"Target patterns": "Target patterns",
|
||||||
"Test Settings and Continue": "Test Settings and Continue",
|
"Test Settings and Continue": "Test Settings and Continue",
|
||||||
"Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.",
|
"Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.",
|
||||||
|
"The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.": "The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.",
|
||||||
"The connection to the server has been configured successfully. As the next step,": "The connection to the server has been configured successfully. As the next step,",
|
"The connection to the server has been configured successfully. As the next step,": "The connection to the server has been configured successfully. As the next step,",
|
||||||
"The delay for consecutive on-demand fetches": "The delay for consecutive on-demand fetches",
|
"The delay for consecutive on-demand fetches": "The delay for consecutive on-demand fetches",
|
||||||
"The files in this Vault are almost identical to the server's.": "The files in this Vault are almost identical to the server's.",
|
"The files in this Vault are almost identical to the server's.": "The files in this Vault are almost identical to the server's.",
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ Export: Export
|
|||||||
"Failed to connect to the server: ${reason}": "Failed to connect to the server: ${reason}"
|
"Failed to connect to the server: ${reason}": "Failed to connect to the server: ${reason}"
|
||||||
Failed to connect to the server. Please check your settings.: Failed to connect to the server. Please check your settings.
|
Failed to connect to the server. Please check your settings.: Failed to connect to the server. Please check your settings.
|
||||||
"Failed to connect to the signalling relay: ${reason}": "Failed to connect to the signalling relay: ${reason}"
|
"Failed to connect to the signalling relay: ${reason}": "Failed to connect to the signalling relay: ${reason}"
|
||||||
|
The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.: The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing.
|
||||||
Failed to create replicator instance.: Failed to create replicator instance.
|
Failed to create replicator instance.: Failed to create replicator instance.
|
||||||
Failed to parse Setup-URI.: Failed to parse Setup-URI.
|
Failed to parse Setup-URI.: Failed to parse Setup-URI.
|
||||||
"Failed:": "Failed:"
|
"Failed:": "Failed:"
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
} from "@vrtmrz/livesync-commonlib/remote-configurations";
|
} from "@vrtmrz/livesync-commonlib/remote-configurations";
|
||||||
import { extractP2PRoomSuffix } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
import { extractP2PRoomSuffix } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||||
import { SetupManager } from "@/modules/features/SetupManager";
|
import { SetupManager } from "@/modules/features/SetupManager";
|
||||||
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
|
|
||||||
import { Menu } from "@/deps";
|
import { Menu } from "@/deps";
|
||||||
import { $msg as translateMessage } from "@/common/translation";
|
import { $msg as translateMessage } from "@/common/translation";
|
||||||
import {
|
import {
|
||||||
@@ -213,9 +212,8 @@
|
|||||||
|
|
||||||
async function createAndSelectP2PRemote() {
|
async function createAndSelectP2PRemote() {
|
||||||
const setupManager = core.getModule(SetupManager);
|
const setupManager = core.getModule(SetupManager);
|
||||||
const dialogManager = setupManager.dialogManager;
|
|
||||||
const currentSettings = core.services.setting.currentSettings();
|
const currentSettings = core.services.setting.currentSettings();
|
||||||
const p2pConf = await dialogManager.openWithExplicitCancel(SetupRemoteP2P, currentSettings);
|
const p2pConf = await setupManager.openP2PSetup(currentSettings);
|
||||||
if (p2pConf === "cancelled" || typeof p2pConf !== "object" || !p2pConf) {
|
if (p2pConf === "cancelled" || typeof p2pConf !== "object" || !p2pConf) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
createOpenReplicationUI(this.app),
|
createOpenReplicationUI(this.app),
|
||||||
createOpenRebuildUI(this.app)
|
createOpenRebuildUI(this.app)
|
||||||
);
|
);
|
||||||
|
setupManager.registerP2PSetupConnectionProbe(replicator.connectionProbe);
|
||||||
useP2PReplicatorCommands(core, replicator);
|
useP2PReplicatorCommands(core, replicator);
|
||||||
useP2PReplicatorUI(core, core, replicator);
|
useP2PReplicatorUI(core, core, replicator);
|
||||||
useRemoteConfiguration(core);
|
useRemoteConfiguration(core);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import type { RemoteConfigurationResult } from "@vrtmrz/livesync-commonlib/compa
|
|||||||
import SetupRemote from "@/modules/features/SetupWizard/dialogs/SetupRemote.svelte";
|
import SetupRemote from "@/modules/features/SetupWizard/dialogs/SetupRemote.svelte";
|
||||||
import SetupRemoteCouchDB from "@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte";
|
import SetupRemoteCouchDB from "@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte";
|
||||||
import SetupRemoteBucket from "@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte";
|
import SetupRemoteBucket from "@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte";
|
||||||
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
|
|
||||||
import type {
|
import type {
|
||||||
SetupRemoteCouchDBInitialData,
|
SetupRemoteCouchDBInitialData,
|
||||||
SetupRemoteCouchDBResultType,
|
SetupRemoteCouchDBResultType,
|
||||||
@@ -217,7 +216,7 @@ export function paneRemoteConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (targetRemoteType === REMOTE_P2P) {
|
if (targetRemoteType === REMOTE_P2P) {
|
||||||
const p2pConf = await dialogManager.openWithExplicitCancel(SetupRemoteP2P, baseSettings);
|
const p2pConf = await setupManager.openP2PSetup(baseSettings);
|
||||||
if (p2pConf === "cancelled" || typeof p2pConf !== "object") {
|
if (p2pConf === "cancelled" || typeof p2pConf !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import type {
|
|||||||
SetupRemoteCouchDBResultType,
|
SetupRemoteCouchDBResultType,
|
||||||
SetupRemoteCouchDBInitialData,
|
SetupRemoteCouchDBInitialData,
|
||||||
SetupRemoteE2EEResultType,
|
SetupRemoteE2EEResultType,
|
||||||
|
SetupRemoteP2PInitialData,
|
||||||
SetupRemoteP2PResultType,
|
SetupRemoteP2PResultType,
|
||||||
SetupRemoteResultType,
|
SetupRemoteResultType,
|
||||||
UseSetupURIResultType,
|
UseSetupURIResultType,
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
type SetupInitialisationMode,
|
type SetupInitialisationMode,
|
||||||
} from "@/serviceFeatures/setupObsidian/setupActivationLifecycle.ts";
|
} from "@/serviceFeatures/setupObsidian/setupActivationLifecycle.ts";
|
||||||
import { isP2PMainRemote } from "@/common/remoteConfiguration.ts";
|
import { isP2PMainRemote } from "@/common/remoteConfiguration.ts";
|
||||||
|
import type { P2PConnectionProbeAdmission } from "@vrtmrz/livesync-commonlib/p2p";
|
||||||
|
|
||||||
function copySettingsForRemoteProfileUpdate(settings: ObsidianLiveSyncSettings): ObsidianLiveSyncSettings {
|
function copySettingsForRemoteProfileUpdate(settings: ObsidianLiveSyncSettings): ObsidianLiveSyncSettings {
|
||||||
return {
|
return {
|
||||||
@@ -94,6 +96,8 @@ export type ApplySettingsWithInitialisationChoiceOptions = {
|
|||||||
* Setup Manager to handle onboarding and configuration setup
|
* Setup Manager to handle onboarding and configuration setup
|
||||||
*/
|
*/
|
||||||
export class SetupManager extends AbstractModule {
|
export class SetupManager extends AbstractModule {
|
||||||
|
private p2pSetupConnectionProbe?: P2PConnectionProbeAdmission;
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * Dialog manager for handling Svelte dialogs
|
// * Dialog manager for handling Svelte dialogs
|
||||||
// */
|
// */
|
||||||
@@ -102,6 +106,26 @@ export class SetupManager extends AbstractModule {
|
|||||||
return this.services.UI.dialogManager;
|
return this.services.UI.dialogManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bind the stable P2P owner's probe view to host-owned Setup dialogues. */
|
||||||
|
registerP2PSetupConnectionProbe(connectionProbe: P2PConnectionProbeAdmission): void {
|
||||||
|
if (this.p2pSetupConnectionProbe && this.p2pSetupConnectionProbe !== connectionProbe) {
|
||||||
|
throw new Error("The P2P Setup connection probe has already been registered.");
|
||||||
|
}
|
||||||
|
this.p2pSetupConnectionProbe = connectionProbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open P2P Setup with the owner-arbitrated connection-probe boundary. */
|
||||||
|
openP2PSetup(settings: P2PSyncSetting): Promise<SetupRemoteP2PResultType> {
|
||||||
|
const connectionProbe = this.p2pSetupConnectionProbe;
|
||||||
|
if (!connectionProbe) {
|
||||||
|
throw new Error("The P2P Setup connection probe is not available.");
|
||||||
|
}
|
||||||
|
return this.dialogManager.openWithExplicitCancel<SetupRemoteP2PResultType, SetupRemoteP2PInitialData>(
|
||||||
|
SetupRemoteP2P,
|
||||||
|
{ settings, connectionProbe }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask which existing data should be authoritative for pending setting changes,
|
* Ask which existing data should be authoritative for pending setting changes,
|
||||||
* then reserve the matching next-start operation before applying them.
|
* then reserve the matching next-start operation before applying them.
|
||||||
@@ -280,10 +304,7 @@ export class SetupManager extends AbstractModule {
|
|||||||
currentSetting: ObsidianLiveSyncSettings,
|
currentSetting: ObsidianLiveSyncSettings,
|
||||||
activate = true
|
activate = true
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const p2pConf = await this.dialogManager.openWithExplicitCancel<SetupRemoteP2PResultType, P2PSyncSetting>(
|
const p2pConf = await this.openP2PSetup(currentSetting);
|
||||||
SetupRemoteP2P,
|
|
||||||
currentSetting
|
|
||||||
);
|
|
||||||
if (p2pConf === "cancelled") {
|
if (p2pConf === "cancelled") {
|
||||||
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||||
return await this.onOnboard(userMode);
|
return await this.onOnboard(userMode);
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import {
|
|||||||
import { SettingService } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
|
import { SettingService } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
|
||||||
import { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
import { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||||
|
import type {
|
||||||
|
P2PConnectionProbeAdmission,
|
||||||
|
P2PConnectionProbeAdmissionResult,
|
||||||
|
P2PConnectionProbeSettings,
|
||||||
|
} from "@vrtmrz/livesync-commonlib/p2p";
|
||||||
|
|
||||||
vi.mock("./SetupWizard/dialogs/Intro.svelte", () => ({ default: {} }));
|
vi.mock("./SetupWizard/dialogs/Intro.svelte", () => ({ default: {} }));
|
||||||
vi.mock("./SetupWizard/dialogs/SelectMethodNewUser.svelte", () => ({ default: {} }));
|
vi.mock("./SetupWizard/dialogs/SelectMethodNewUser.svelte", () => ({ default: {} }));
|
||||||
@@ -124,11 +129,23 @@ function createSetupManager() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const p2pSetupConnectionProbe: P2PConnectionProbeAdmission = {
|
||||||
|
async run<T>(
|
||||||
|
_settings: P2PConnectionProbeSettings,
|
||||||
|
runOwnedTrial: () => Promise<T>
|
||||||
|
): Promise<P2PConnectionProbeAdmissionResult<T>> {
|
||||||
|
return { status: "trial", result: await runOwnedTrial() };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const manager = new SetupManager(core);
|
||||||
|
manager.registerP2PSetupConnectionProbe(p2pSetupConnectionProbe);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
manager: new SetupManager(core),
|
manager,
|
||||||
setting,
|
setting,
|
||||||
dialogManager,
|
dialogManager,
|
||||||
core,
|
core,
|
||||||
|
p2pSetupConnectionProbe,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +155,19 @@ describe("SetupManager", () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens P2P Setup with the registered owner admission", async () => {
|
||||||
|
const { manager, setting, dialogManager, p2pSetupConnectionProbe } = createSetupManager();
|
||||||
|
const settings = setting.currentSettings();
|
||||||
|
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
|
||||||
|
|
||||||
|
await expect(manager.openP2PSetup(settings)).resolves.toBe("cancelled");
|
||||||
|
|
||||||
|
expect(dialogManager.openWithExplicitCancel).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ settings, connectionProbe: p2pSetupConnectionProbe })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("starts manual new-user setup from the recommended new-Vault settings", async () => {
|
it("starts manual new-user setup from the recommended new-Vault settings", async () => {
|
||||||
const { manager, dialogManager } = createSetupManager();
|
const { manager, dialogManager } = createSetupManager();
|
||||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("configure-manually");
|
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("configure-manually");
|
||||||
|
|||||||
@@ -36,10 +36,14 @@
|
|||||||
import { getDialogContext, type GuestDialogProps } from "@/modules/services/LiveSyncUI/svelteDialog";
|
import { getDialogContext, type GuestDialogProps } from "@/modules/services/LiveSyncUI/svelteDialog";
|
||||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
import ExtraItems from "@/modules/services/LiveSyncUI/components/ExtraItems.svelte";
|
import ExtraItems from "@/modules/services/LiveSyncUI/components/ExtraItems.svelte";
|
||||||
import { TYPE_CANCELLED, type SetupRemoteP2PResultType } from "./setupDialogTypes";
|
import {
|
||||||
|
TYPE_CANCELLED,
|
||||||
|
type SetupRemoteP2PInitialData,
|
||||||
|
type SetupRemoteP2PResultType,
|
||||||
|
} from "./setupDialogTypes";
|
||||||
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
|
||||||
import { $msg as translateMessage } from "@/common/translation";
|
import { $msg as translateMessage } from "@/common/translation";
|
||||||
import { probeP2PSetupConnection } from "./p2pSetupConnectionProbe";
|
import { coordinateP2PSetupConnectionProbe, probeP2PSetupConnection } from "./p2pSetupConnectionProbe";
|
||||||
|
|
||||||
const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS);
|
const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS);
|
||||||
let syncSetting = $state<P2PConnectionInfo>({ ...default_setting });
|
let syncSetting = $state<P2PConnectionInfo>({ ...default_setting });
|
||||||
@@ -48,18 +52,18 @@
|
|||||||
let error = $state("");
|
let error = $state("");
|
||||||
let connectionPathResetNotice = $state(false);
|
let connectionPathResetNotice = $state(false);
|
||||||
const hasValidTurnServer = $derived(hasValidP2PTurnServerUrl(syncSetting.P2P_turnServers ?? ""));
|
const hasValidTurnServer = $derived(hasValidP2PTurnServerUrl(syncSetting.P2P_turnServers ?? ""));
|
||||||
type Props = GuestDialogProps<SetupRemoteP2PResultType, P2PSyncSetting>;
|
type Props = GuestDialogProps<SetupRemoteP2PResultType, SetupRemoteP2PInitialData>;
|
||||||
|
|
||||||
const { setResult, getInitialData }: Props = $props();
|
const { setResult, getInitialData }: Props = $props();
|
||||||
|
let connectionProbe: SetupRemoteP2PInitialData["connectionProbe"] | undefined;
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let initialData: P2PSyncSetting | undefined = undefined;
|
const initialData = getInitialData?.();
|
||||||
if (getInitialData) {
|
connectionProbe = initialData?.connectionProbe;
|
||||||
initialData = getInitialData();
|
const initialSettings = initialData?.settings;
|
||||||
if (initialData) {
|
if (initialSettings) {
|
||||||
copyTo(initialData, syncSetting);
|
copyTo(initialSettings, syncSetting);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const initialPeerName = (initialData?.P2P_DevicePeerName ?? "").trim();
|
const initialPeerName = (initialSettings?.P2P_DevicePeerName ?? "").trim();
|
||||||
if (initialPeerName !== "") {
|
if (initialPeerName !== "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -97,58 +101,74 @@
|
|||||||
try {
|
try {
|
||||||
processing = true;
|
processing = true;
|
||||||
const trialRemoteSetting = generateSetting();
|
const trialRemoteSetting = generateSetting();
|
||||||
const map = new Map<string, string>();
|
const admission = connectionProbe;
|
||||||
const store = {
|
if (!admission) {
|
||||||
get: (key: string) => {
|
throw new Error("The P2P Setup connection probe is not available.");
|
||||||
return Promise.resolve(map.get(key) || null);
|
|
||||||
},
|
|
||||||
set: (key: string, value: any) => {
|
|
||||||
map.set(key, value);
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
delete: (key: string) => {
|
|
||||||
map.delete(key);
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
keys: () => {
|
|
||||||
return Promise.resolve(Array.from(map.keys()));
|
|
||||||
},
|
|
||||||
get db() {
|
|
||||||
return Promise.resolve(this);
|
|
||||||
},
|
|
||||||
} as SimpleStore<any>;
|
|
||||||
|
|
||||||
const dummyPouch = new PouchDB<EntryDoc>("dummy");
|
|
||||||
const env: ReplicatorHostEnv = {
|
|
||||||
events: context.context.events,
|
|
||||||
translate: context.context.translate,
|
|
||||||
settings: trialRemoteSetting,
|
|
||||||
processReplicatedDocs: async (_docs: any[]) => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
confirm: context.services.confirm,
|
|
||||||
db: dummyPouch,
|
|
||||||
simpleStore: store,
|
|
||||||
deviceName: syncSetting.P2P_DevicePeerName || "unnamed-device",
|
|
||||||
platform: "setup-wizard",
|
|
||||||
};
|
|
||||||
const replicator = new TrysteroReplicator(env);
|
|
||||||
try {
|
|
||||||
const result = await probeP2PSetupConnection(replicator);
|
|
||||||
if (!result.ok) {
|
|
||||||
return translateMessage("Failed to connect to the signalling relay: ${reason}", {
|
|
||||||
reason: `${result.reason}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await replicator.dispose();
|
|
||||||
await dummyPouch.destroy();
|
|
||||||
} catch (e) {
|
|
||||||
Logger(e, LOG_LEVEL_VERBOSE, "setup-p2p-cleanup");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const result = await coordinateP2PSetupConnectionProbe(admission, trialRemoteSetting, async () => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
const store = {
|
||||||
|
get: (key: string) => {
|
||||||
|
return Promise.resolve(map.get(key) || null);
|
||||||
|
},
|
||||||
|
set: (key: string, value: any) => {
|
||||||
|
map.set(key, value);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
delete: (key: string) => {
|
||||||
|
map.delete(key);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
keys: () => {
|
||||||
|
return Promise.resolve(Array.from(map.keys()));
|
||||||
|
},
|
||||||
|
get db() {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
},
|
||||||
|
} as SimpleStore<any>;
|
||||||
|
|
||||||
|
const dummyPouch = new PouchDB<EntryDoc>("dummy");
|
||||||
|
let replicator: TrysteroReplicator | undefined;
|
||||||
|
try {
|
||||||
|
const env: ReplicatorHostEnv = {
|
||||||
|
events: context.context.events,
|
||||||
|
translate: context.context.translate,
|
||||||
|
settings: trialRemoteSetting,
|
||||||
|
processReplicatedDocs: async (_docs: any[]) => {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
confirm: context.services.confirm,
|
||||||
|
db: dummyPouch,
|
||||||
|
simpleStore: store,
|
||||||
|
deviceName: syncSetting.P2P_DevicePeerName || "unnamed-device",
|
||||||
|
platform: "setup-wizard",
|
||||||
|
};
|
||||||
|
replicator = new TrysteroReplicator(env);
|
||||||
|
return await probeP2PSetupConnection(replicator);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await replicator?.dispose();
|
||||||
|
} catch (e) {
|
||||||
|
Logger(e, LOG_LEVEL_VERBOSE, "setup-p2p-replicator-cleanup");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await dummyPouch.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
Logger(e, LOG_LEVEL_VERBOSE, "setup-p2p-database-cleanup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!result.ok) {
|
||||||
|
if ("kind" in result && result.kind === "blocked") {
|
||||||
|
return translateMessage(
|
||||||
|
"The connection test cannot add a signalling relay while P2P is active. Use the active relay settings, or disconnect P2P before testing."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return translateMessage("Failed to connect to the signalling relay: ${reason}", {
|
||||||
|
reason: `${result.reason}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return "";
|
||||||
} finally {
|
} finally {
|
||||||
processing = false;
|
processing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
export type P2PSetupConnectionProbeResult = { ok: true } | { ok: false; reason: string };
|
import {
|
||||||
|
ACTIVE_P2P_RELAY_BINDING_CONFLICT,
|
||||||
|
type P2PConnectionProbeAdmission,
|
||||||
|
type P2PConnectionProbeSettings,
|
||||||
|
} from "@vrtmrz/livesync-commonlib/p2p";
|
||||||
|
|
||||||
|
export type P2PSetupConnectionProbeResult =
|
||||||
|
| { readonly ok: true }
|
||||||
|
| { readonly ok: false; readonly reason: string }
|
||||||
|
| {
|
||||||
|
readonly ok: false;
|
||||||
|
readonly kind: "blocked";
|
||||||
|
readonly reason: typeof ACTIVE_P2P_RELAY_BINDING_CONFLICT;
|
||||||
|
};
|
||||||
|
|
||||||
export interface P2PSetupConnectionProbe {
|
export interface P2PSetupConnectionProbe {
|
||||||
setOnSetup(): void | Promise<void>;
|
setOnSetup(): void | Promise<void>;
|
||||||
@@ -6,6 +19,25 @@ export interface P2PSetupConnectionProbe {
|
|||||||
open(): Promise<void>;
|
open(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Interpret the stable P2P owner's admission without constructing transport eagerly. */
|
||||||
|
export async function coordinateP2PSetupConnectionProbe(
|
||||||
|
admission: P2PConnectionProbeAdmission,
|
||||||
|
trialSettings: P2PConnectionProbeSettings,
|
||||||
|
runOwnedTrial: () => Promise<P2PSetupConnectionProbeResult>
|
||||||
|
): Promise<P2PSetupConnectionProbeResult> {
|
||||||
|
const settlement = await admission.run(trialSettings, runOwnedTrial);
|
||||||
|
if (settlement.status === "observed-active") return { ok: true };
|
||||||
|
if (settlement.status === "blocked") {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
kind: "blocked",
|
||||||
|
reason: settlement.reason,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return settlement.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open one separately owned signalling connection and report its outcome. */
|
||||||
export async function probeP2PSetupConnection(
|
export async function probeP2PSetupConnection(
|
||||||
replicator: P2PSetupConnectionProbe
|
replicator: P2PSetupConnectionProbe
|
||||||
): Promise<P2PSetupConnectionProbeResult> {
|
): Promise<P2PSetupConnectionProbeResult> {
|
||||||
|
|||||||
@@ -1,7 +1,69 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { probeP2PSetupConnection } from "./p2pSetupConnectionProbe";
|
import { ACTIVE_P2P_RELAY_BINDING_CONFLICT, type P2PConnectionProbeAdmission } from "@vrtmrz/livesync-commonlib/p2p";
|
||||||
|
import {
|
||||||
|
coordinateP2PSetupConnectionProbe,
|
||||||
|
probeP2PSetupConnection,
|
||||||
|
type P2PSetupConnectionProbeResult,
|
||||||
|
} from "./p2pSetupConnectionProbe";
|
||||||
|
|
||||||
describe("P2P setup connection probe", () => {
|
describe("P2P setup connection probe", () => {
|
||||||
|
it("uses a compatible active signalling connection without constructing a trial", async () => {
|
||||||
|
const runOwnedTrial = vi.fn(async (): Promise<P2PSetupConnectionProbeResult> => ({ ok: true }));
|
||||||
|
const admission: P2PConnectionProbeAdmission = {
|
||||||
|
run: vi.fn(async () => ({ status: "observed-active" }) as const),
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
coordinateP2PSetupConnectionProbe(admission, { P2P_relays: "wss://relay.example.com" }, runOwnedTrial)
|
||||||
|
).resolves.toEqual({ ok: true });
|
||||||
|
|
||||||
|
expect(admission.run).toHaveBeenCalledOnce();
|
||||||
|
expect(runOwnedTrial).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the typed blocked reason without opening an incompatible trial", async () => {
|
||||||
|
const runOwnedTrial = vi.fn(async (): Promise<P2PSetupConnectionProbeResult> => ({ ok: true }));
|
||||||
|
const admission: P2PConnectionProbeAdmission = {
|
||||||
|
run: vi.fn(
|
||||||
|
async () =>
|
||||||
|
({
|
||||||
|
status: "blocked",
|
||||||
|
reason: ACTIVE_P2P_RELAY_BINDING_CONFLICT,
|
||||||
|
}) as const
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
coordinateP2PSetupConnectionProbe(
|
||||||
|
admission,
|
||||||
|
{ P2P_relays: "wss://another-relay.example.com" },
|
||||||
|
runOwnedTrial
|
||||||
|
)
|
||||||
|
).resolves.toEqual({
|
||||||
|
ok: false,
|
||||||
|
kind: "blocked",
|
||||||
|
reason: ACTIVE_P2P_RELAY_BINDING_CONFLICT,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(admission.run).toHaveBeenCalledOnce();
|
||||||
|
expect(runOwnedTrial).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs and returns the complete owned trial continuation when no room is active", async () => {
|
||||||
|
const trialResult = { ok: false, reason: "relay unavailable" } as const;
|
||||||
|
const runOwnedTrial = vi.fn(async (): Promise<P2PSetupConnectionProbeResult> => trialResult);
|
||||||
|
const admission: P2PConnectionProbeAdmission = {
|
||||||
|
run: vi.fn(async (_settings, trial) => ({ status: "trial", result: await trial() }) as const),
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
coordinateP2PSetupConnectionProbe(admission, { P2P_relays: "wss://relay.example.com" }, runOwnedTrial)
|
||||||
|
).resolves.toEqual(trialResult);
|
||||||
|
|
||||||
|
expect(admission.run).toHaveBeenCalledOnce();
|
||||||
|
expect(runOwnedTrial).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts an empty room after the signalling connection opens", async () => {
|
it("accepts an empty room after the signalling connection opens", async () => {
|
||||||
const replicator = {
|
const replicator = {
|
||||||
knownAdvertisements: [],
|
knownAdvertisements: [],
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import type {
|
|||||||
EncryptionSettings,
|
EncryptionSettings,
|
||||||
ObsidianLiveSyncSettings,
|
ObsidianLiveSyncSettings,
|
||||||
P2PConnectionInfo,
|
P2PConnectionInfo,
|
||||||
|
P2PSyncSetting,
|
||||||
} from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
|
} from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
|
||||||
|
import type { P2PConnectionProbeAdmission } from "@vrtmrz/livesync-commonlib/p2p";
|
||||||
|
|
||||||
export const TYPE_IDENTICAL = "identical";
|
export const TYPE_IDENTICAL = "identical";
|
||||||
export const TYPE_INDEPENDENT = "independent";
|
export const TYPE_INDEPENDENT = "independent";
|
||||||
@@ -119,5 +121,9 @@ export type SetupRemoteCouchDBInitialData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SetupRemoteP2PResultType = typeof TYPE_CANCELLED | P2PConnectionInfo;
|
export type SetupRemoteP2PResultType = typeof TYPE_CANCELLED | P2PConnectionInfo;
|
||||||
|
export type SetupRemoteP2PInitialData = {
|
||||||
|
settings: P2PSyncSetting;
|
||||||
|
connectionProbe: P2PConnectionProbeAdmission;
|
||||||
|
};
|
||||||
|
|
||||||
export type ScanQRCodeResultType = typeof TYPE_CLOSE;
|
export type ScanQRCodeResultType = typeof TYPE_CLOSE;
|
||||||
|
|||||||
Reference in New Issue
Block a user