mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-01 08:17:07 +00:00
Route P2P Setup probes through the room owner
This commit is contained in:
@@ -36,10 +36,14 @@
|
||||
import { getDialogContext, type GuestDialogProps } from "@/modules/services/LiveSyncUI/svelteDialog";
|
||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
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 { $msg as translateMessage } from "@/common/translation";
|
||||
import { probeP2PSetupConnection } from "./p2pSetupConnectionProbe";
|
||||
import { coordinateP2PSetupConnectionProbe, probeP2PSetupConnection } from "./p2pSetupConnectionProbe";
|
||||
|
||||
const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS);
|
||||
let syncSetting = $state<P2PConnectionInfo>({ ...default_setting });
|
||||
@@ -48,18 +52,18 @@
|
||||
let error = $state("");
|
||||
let connectionPathResetNotice = $state(false);
|
||||
const hasValidTurnServer = $derived(hasValidP2PTurnServerUrl(syncSetting.P2P_turnServers ?? ""));
|
||||
type Props = GuestDialogProps<SetupRemoteP2PResultType, P2PSyncSetting>;
|
||||
type Props = GuestDialogProps<SetupRemoteP2PResultType, SetupRemoteP2PInitialData>;
|
||||
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
let connectionProbe: SetupRemoteP2PInitialData["connectionProbe"] | undefined;
|
||||
onMount(() => {
|
||||
let initialData: P2PSyncSetting | undefined = undefined;
|
||||
if (getInitialData) {
|
||||
initialData = getInitialData();
|
||||
if (initialData) {
|
||||
copyTo(initialData, syncSetting);
|
||||
}
|
||||
const initialData = getInitialData?.();
|
||||
connectionProbe = initialData?.connectionProbe;
|
||||
const initialSettings = initialData?.settings;
|
||||
if (initialSettings) {
|
||||
copyTo(initialSettings, syncSetting);
|
||||
}
|
||||
const initialPeerName = (initialData?.P2P_DevicePeerName ?? "").trim();
|
||||
const initialPeerName = (initialSettings?.P2P_DevicePeerName ?? "").trim();
|
||||
if (initialPeerName !== "") {
|
||||
return;
|
||||
}
|
||||
@@ -97,58 +101,74 @@
|
||||
try {
|
||||
processing = true;
|
||||
const trialRemoteSetting = generateSetting();
|
||||
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");
|
||||
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 admission = connectionProbe;
|
||||
if (!admission) {
|
||||
throw new Error("The P2P Setup connection probe is not available.");
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
setOnSetup(): void | Promise<void>;
|
||||
@@ -6,6 +19,25 @@ export interface P2PSetupConnectionProbe {
|
||||
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(
|
||||
replicator: P2PSetupConnectionProbe
|
||||
): Promise<P2PSetupConnectionProbeResult> {
|
||||
|
||||
@@ -1,7 +1,69 @@
|
||||
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", () => {
|
||||
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 () => {
|
||||
const replicator = {
|
||||
knownAdvertisements: [],
|
||||
|
||||
@@ -4,7 +4,9 @@ import type {
|
||||
EncryptionSettings,
|
||||
ObsidianLiveSyncSettings,
|
||||
P2PConnectionInfo,
|
||||
P2PSyncSetting,
|
||||
} 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_INDEPENDENT = "independent";
|
||||
@@ -119,5 +121,9 @@ export type SetupRemoteCouchDBInitialData = {
|
||||
};
|
||||
|
||||
export type SetupRemoteP2PResultType = typeof TYPE_CANCELLED | P2PConnectionInfo;
|
||||
export type SetupRemoteP2PInitialData = {
|
||||
settings: P2PSyncSetting;
|
||||
connectionProbe: P2PConnectionProbeAdmission;
|
||||
};
|
||||
|
||||
export type ScanQRCodeResultType = typeof TYPE_CLOSE;
|
||||
|
||||
Reference in New Issue
Block a user