Adopt focused P2P service views

This commit is contained in:
vorotamoroz
2026-08-28 03:04:53 +00:00
parent cc000f2cdf
commit ad83ae858d
20 changed files with 276 additions and 112 deletions
+3
View File
@@ -37,6 +37,7 @@ import { usePrepareDatabaseForUse } from "@vrtmrz/livesync-commonlib/compat/serv
import type { Constructor } from "@vrtmrz/livesync-commonlib/compat/common/utils.type"; import type { Constructor } from "@vrtmrz/livesync-commonlib/compat/common/utils.type";
import { import {
CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_APPLICABLE,
CENTRAL_REMOTE_REPLICATION_READINESS,
defineReplicatorProviderDefinitions, defineReplicatorProviderDefinitions,
supportedOpenReplicationContinuous, supportedOpenReplicationContinuous,
supportedOpenReplicationOneShot, supportedOpenReplicationOneShot,
@@ -158,6 +159,7 @@ export class LiveSyncBaseCore<
[REMOTE_COUCHDB]: { [REMOTE_COUCHDB]: {
kind: REMOTE_COUCHDB, kind: REMOTE_COUCHDB,
diagnosticName: "CouchDB", diagnosticName: "CouchDB",
readiness: CENTRAL_REMOTE_REPLICATION_READINESS,
isConfigured: (settings) => isConfigured: (settings) =>
settings.remoteType === REMOTE_COUCHDB && settings.remoteType === REMOTE_COUCHDB &&
!!settings.couchDB_URI?.trim() && !!settings.couchDB_URI?.trim() &&
@@ -171,6 +173,7 @@ export class LiveSyncBaseCore<
[REMOTE_MINIO]: { [REMOTE_MINIO]: {
kind: REMOTE_MINIO, kind: REMOTE_MINIO,
diagnosticName: "Object Storage", diagnosticName: "Object Storage",
readiness: CENTRAL_REMOTE_REPLICATION_READINESS,
isConfigured: (settings) => isConfigured: (settings) =>
settings.remoteType === REMOTE_MINIO && !!settings.endpoint?.trim() && !!settings.bucket?.trim(), settings.remoteType === REMOTE_MINIO && !!settings.endpoint?.trim() && !!settings.bucket?.trim(),
create: (_settings) => Promise.resolve(new LiveSyncJournalReplicator(this)), create: (_settings) => Promise.resolve(new LiveSyncJournalReplicator(this)),
+1 -1
View File
@@ -53,7 +53,7 @@ export class P2PCheckSession {
try { try {
await runtime.start(); await runtime.start();
await runtime.currentReplicator.makeSureOpened(); await runtime.p2p.transportLifecycle.connect();
} catch (error) { } catch (error) {
await this.stop(); await this.stop();
throw error; throw error;
+5 -12
View File
@@ -3,10 +3,9 @@ import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFu
import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon"; import type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector"; import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector";
import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature"; import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
import { ServiceContext, type LiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context"; import { ServiceContext, type LiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context";
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import { unique } from "octagonal-wheels/collection"; import { unique } from "octagonal-wheels/collection";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
@@ -48,7 +47,7 @@ function removeFromList(item: string, list: string): string {
export class WebPeerRuntime { export class WebPeerRuntime {
readonly context: ServiceContext; readonly context: ServiceContext;
readonly services: LiveSyncBrowserServiceHub<ServiceContext>; readonly services: LiveSyncBrowserServiceHub<ServiceContext>;
readonly p2p: UseP2PReplicatorResult; readonly p2p: P2PServiceViews;
readonly p2pLogCollector: P2PLogCollector; readonly p2pLogCollector: P2PLogCollector;
readonly paneHost: P2PReplicatorPaneHost; readonly paneHost: P2PReplicatorPaneHost;
@@ -87,10 +86,6 @@ export class WebPeerRuntime {
return this.context.events; return this.context.events;
} }
get currentReplicator(): LiveSyncTrysteroReplicator {
return this.p2p.replicator;
}
get settings(): P2PSyncSetting { get settings(): P2PSyncSetting {
return this.services.setting.currentSettings(); return this.services.setting.currentSettings();
} }
@@ -119,9 +114,7 @@ export class WebPeerRuntime {
} }
this.services.appLifecycle.markIsReady(); this.services.appLifecycle.markIsReady();
this.events.emitEvent(EVENT_LAYOUT_READY); this.events.emitEvent(EVENT_LAYOUT_READY);
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) { await this.services.appLifecycle.onResumed();
compatGlobal.setTimeout(() => void this.currentReplicator.open(), 100);
}
return this; return this;
} }
@@ -151,12 +144,12 @@ export class WebPeerRuntime {
this.menu = new Menu() this.menu = new Menu()
.addItem((item) => .addItem((item) =>
item.setTitle("📥 Only fetch").onClick(async () => { item.setTitle("📥 Only fetch").onClick(async () => {
await this.currentReplicator.replicateFrom(peer.peerId); await this.p2p.targetedTransfer.pullFromPeer(peer.peerId);
}) })
) )
.addItem((item) => .addItem((item) =>
item.setTitle("📤 Only send").onClick(async () => { item.setTitle("📤 Only send").onClick(async () => {
await this.currentReplicator.requestSynchroniseToPeer(peer.peerId); await this.p2p.targetedTransfer.requestPushToPeer(peer.peerId);
}) })
) )
.addSeparator() .addSeparator()
@@ -1,7 +1,7 @@
import { App, Modal } from "@/deps.ts"; import { App, Modal } from "@/deps.ts";
import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte"; import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte";
import { mount, unmount } from "svelte"; import { mount, unmount } from "svelte";
import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
export type P2POpenReplicationModalCallback = { export type P2POpenReplicationModalCallback = {
onSync: (peerId: string) => Promise<void>; onSync: (peerId: string) => Promise<void>;
@@ -9,7 +9,7 @@ export type P2POpenReplicationModalCallback = {
}; };
export class P2POpenReplicationModal extends Modal { export class P2POpenReplicationModal extends Modal {
liveSyncReplicator: LiveSyncTrysteroReplicator; p2p: P2PServiceViews;
callback?: P2POpenReplicationModalCallback; callback?: P2POpenReplicationModalCallback;
component?: ReturnType<typeof mount>; component?: ReturnType<typeof mount>;
showResult: boolean; showResult: boolean;
@@ -19,7 +19,7 @@ export class P2POpenReplicationModal extends Modal {
constructor( constructor(
app: App, app: App,
liveSyncReplicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews,
callback?: P2POpenReplicationModalCallback, callback?: P2POpenReplicationModalCallback,
showResult: boolean = false, showResult: boolean = false,
title: string = "P2P Replication", title: string = "P2P Replication",
@@ -27,7 +27,7 @@ export class P2POpenReplicationModal extends Modal {
rebuildMode: boolean = false rebuildMode: boolean = false
) { ) {
super(app); super(app);
this.liveSyncReplicator = liveSyncReplicator; this.p2p = p2p;
this.callback = callback; this.callback = callback;
this.showResult = showResult; this.showResult = showResult;
this.title = title; this.title = title;
@@ -57,7 +57,7 @@ export class P2POpenReplicationModal extends Modal {
this.component = mount(P2POpenReplicationPane, { this.component = mount(P2POpenReplicationPane, {
target: contentEl, target: contentEl,
props: { props: {
liveSyncReplicator: this.liveSyncReplicator, p2p: this.p2p,
onSync: (peerId: string) => this.onSync(peerId), onSync: (peerId: string) => this.onSync(peerId),
onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId), onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId),
onClose: () => this.close(), onClose: () => this.close(),
@@ -9,13 +9,13 @@
// import type { TrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator"; // import type { TrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import { delay, fireAndForget } from "octagonal-wheels/promises"; import { delay, fireAndForget } from "octagonal-wheels/promises";
import P2PServerStatusCard from "./P2PServerStatusCard.svelte"; import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
import { $msg as translateMessage } from "@/common/translation"; import { $msg as translateMessage } from "@/common/translation";
interface Props { interface Props {
liveSyncReplicator: LiveSyncTrysteroReplicator; p2p: P2PServiceViews;
onSync: (_peerId: string) => Promise<void>; onSync: (_peerId: string) => Promise<void>;
onSyncAndClose: (_peerId: string) => Promise<void>; onSyncAndClose: (_peerId: string) => Promise<void>;
onClose: () => void; onClose: () => void;
@@ -23,15 +23,14 @@
rebuildMode?: boolean; rebuildMode?: boolean;
} }
let { onSync, onSyncAndClose, onClose, showResult, liveSyncReplicator, rebuildMode = false }: Props = $props(); let { onSync, onSyncAndClose, onClose, showResult, p2p, rebuildMode = false }: Props = $props();
const getLiveSyncReplicator = () => liveSyncReplicator;
let serverInfo = $state<P2PServerInfo | undefined>(undefined); let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let syncingPeerId = $state<string | null>(null); let syncingPeerId = $state<string | null>(null);
const logLevel = $derived(showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); const logLevel = $derived(showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
async function requestServerStatus() { async function requestServerStatus() {
await liveSyncReplicator.requestStatus(); p2p.diagnostics.requestStatus();
eventHub.emitEvent(EVENT_REQUEST_STATUS); eventHub.emitEvent(EVENT_REQUEST_STATUS);
} }
onMount(() => { onMount(() => {
@@ -73,7 +72,7 @@
async function disconnect() { async function disconnect() {
try { try {
await liveSyncReplicator.close(); await p2p.transportLifecycle.disconnect();
Logger("Signalling connection closed.", logLevel); Logger("Signalling connection closed.", logLevel);
} catch (e) { } catch (e) {
Logger(`Failed to close signalling connection: ${e instanceof Error ? e.message : String(e)}`, logLevel); Logger(`Failed to close signalling connection: ${e instanceof Error ? e.message : String(e)}`, logLevel);
@@ -100,7 +99,7 @@
</script> </script>
<div class="p2p-container"> <div class="p2p-container">
<P2PServerStatusCard {getLiveSyncReplicator} showBroadcastToggle={false} /> <P2PServerStatusCard {p2p} showBroadcastToggle={false} />
<div class="peers-section"> <div class="peers-section">
<h3>{translateMessage("Available Peers")}</h3> <h3>{translateMessage("Available Peers")}</h3>
@@ -2,6 +2,7 @@ import type { App } from "@/deps.ts";
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import { P2POpenReplicationModal } from "./P2POpenReplicationModal"; import { P2POpenReplicationModal } from "./P2POpenReplicationModal";
/** /**
@@ -15,8 +16,8 @@ import { P2POpenReplicationModal } from "./P2POpenReplicationModal";
*/ */
export function createOpenReplicationUI( export function createOpenReplicationUI(
app: App app: App
): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise<boolean | void> { ): (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void> {
return (replicator: LiveSyncTrysteroReplicator) => return (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) =>
(showResult: boolean): Promise<boolean | void> => { (showResult: boolean): Promise<boolean | void> => {
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
return new Promise<boolean | void>((resolve) => { return new Promise<boolean | void>((resolve) => {
@@ -57,7 +58,7 @@ export function createOpenReplicationUI(
}; };
const modal = new P2POpenReplicationModal( const modal = new P2POpenReplicationModal(
app, app,
replicator, p2p,
{ {
onSync: (peerId: string) => synchronise(peerId, false), onSync: (peerId: string) => synchronise(peerId, false),
onSyncAndClose: (peerId: string) => synchronise(peerId, true), onSyncAndClose: (peerId: string) => synchronise(peerId, true),
@@ -85,8 +86,8 @@ export function createOpenReplicationUI(
*/ */
export function createOpenRebuildUI( export function createOpenRebuildUI(
app: App app: App
): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise<boolean | void> { ): (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void> {
return (replicator: LiveSyncTrysteroReplicator) => return (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) =>
(showResult: boolean): Promise<boolean | void> => { (showResult: boolean): Promise<boolean | void> => {
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO; const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
return new Promise<boolean | void>((resolve) => { return new Promise<boolean | void>((resolve) => {
@@ -132,7 +133,7 @@ export function createOpenRebuildUI(
const modal = new P2POpenReplicationModal( const modal = new P2POpenReplicationModal(
app, app,
replicator, p2p,
{ {
onSync: doRebuild, onSync: doRebuild,
onSyncAndClose: doRebuild, onSyncAndClose: doRebuild,
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const modalState = vi.hoisted(() => ({ const modalState = vi.hoisted(() => ({
instances: [] as Array<{ instances: [] as Array<{
p2p: unknown;
callback: { callback: {
onSync: (peerId: string) => Promise<void>; onSync: (peerId: string) => Promise<void>;
onSyncAndClose: (peerId: string) => Promise<void>; onSyncAndClose: (peerId: string) => Promise<void>;
@@ -15,18 +16,20 @@ vi.mock("@/deps.ts", () => ({ App: class {} }));
vi.mock("./P2POpenReplicationModal", () => ({ vi.mock("./P2POpenReplicationModal", () => ({
P2POpenReplicationModal: class { P2POpenReplicationModal: class {
p2p;
callback; callback;
onClosed; onClosed;
open = vi.fn(); open = vi.fn();
constructor( constructor(
_app: unknown, _app: unknown,
_replicator: unknown, p2p: unknown,
callback: (typeof modalState.instances)[number]["callback"], callback: (typeof modalState.instances)[number]["callback"],
_showResult: boolean, _showResult: boolean,
_title?: string, _title?: string,
onClosed?: () => void onClosed?: () => void
) { ) {
this.p2p = p2p;
this.callback = callback; this.callback = callback;
this.onClosed = onClosed; this.onClosed = onClosed;
modalState.instances.push(this); modalState.instances.push(this);
@@ -46,15 +49,21 @@ function createReplicator() {
} as any; } as any;
} }
function createP2PServiceViews() {
return { transportLifecycle: {}, diagnostics: {} } as any;
}
describe("createOpenReplicationUI", () => { describe("createOpenReplicationUI", () => {
beforeEach(() => { beforeEach(() => {
modalState.instances.length = 0; modalState.instances.length = 0;
}); });
it("settles a cancelled peer-selection session when the modal closes", async () => { it("settles a cancelled peer-selection session when the modal closes", async () => {
const session = createOpenReplicationUI({} as any)(createReplicator())(true); const p2p = createP2PServiceViews();
const session = createOpenReplicationUI({} as any)(createReplicator(), p2p)(true);
const modal = modalState.instances[0]; const modal = modalState.instances[0];
expect(modal.p2p).toBe(p2p);
expect(modal.onClosed).toBeTypeOf("function"); expect(modal.onClosed).toBeTypeOf("function");
modal.onClosed?.(); modal.onClosed?.();
@@ -63,7 +72,7 @@ describe("createOpenReplicationUI", () => {
it("keeps repeated synchronisation inside the session boundary until the modal closes", async () => { it("keeps repeated synchronisation inside the session boundary until the modal closes", async () => {
const replicator = createReplicator(); const replicator = createReplicator();
const session = createOpenReplicationUI({} as any)(replicator)(true); const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0]; const modal = modalState.instances[0];
let settled = false; let settled = false;
void session.finally(() => { void session.finally(() => {
@@ -91,7 +100,7 @@ describe("createOpenReplicationUI", () => {
finishPull = resolve; finishPull = resolve;
}) })
); );
const session = createOpenReplicationUI({} as any)(replicator)(true); const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0]; const modal = modalState.instances[0];
let settled = false; let settled = false;
void session.finally(() => { void session.finally(() => {
@@ -111,7 +120,7 @@ describe("createOpenReplicationUI", () => {
it("closes the P2P connection after a successful sync-and-close action", async () => { it("closes the P2P connection after a successful sync-and-close action", async () => {
const replicator = createReplicator(); const replicator = createReplicator();
const session = createOpenReplicationUI({} as any)(replicator)(true); const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0]; const modal = modalState.instances[0];
await modal.callback.onSyncAndClose("peer-a"); await modal.callback.onSyncAndClose("peer-a");
@@ -143,7 +152,7 @@ describe("createOpenRebuildUI", () => {
finishPull = resolve; finishPull = resolve;
}) })
); );
const session = createOpenRebuildUI({} as any)(replicator)(true); const session = createOpenRebuildUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0]; const modal = modalState.instances[0];
let settled = false; let settled = false;
void session.finally(() => { void session.finally(() => {
@@ -166,7 +175,7 @@ describe("createOpenRebuildUI", () => {
it("does not complete Fetch when the rebuild dialogue closes without selecting a peer", async () => { it("does not complete Fetch when the rebuild dialogue closes without selecting a peer", async () => {
const replicator = createReplicator(); const replicator = createReplicator();
const session = createOpenRebuildUI({} as any)(replicator)(true); const session = createOpenRebuildUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0]; const modal = modalState.instances[0];
modal.onClosed?.(); modal.onClosed?.();
@@ -13,7 +13,6 @@
type PeerInfo, type PeerInfo,
type P2PServerInfo, type P2PServerInfo,
EVENT_SERVER_STATUS, EVENT_SERVER_STATUS,
EVENT_REQUEST_STATUS,
EVENT_P2P_REPLICATOR_STATUS, EVENT_P2P_REPLICATOR_STATUS,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer"; } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
import type { P2PReplicatorStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator"; import type { P2PReplicatorStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
@@ -29,7 +28,6 @@
let services = $derived(host.services); let services = $derived(host.services);
let events = $derived(services.context.events); let events = $derived(services.context.events);
const currentSettings = () => services.setting.currentSettings() as P2PSyncSetting; const currentSettings = () => services.setting.currentSettings() as P2PSyncSetting;
const currentReplicator = () => host.p2p.replicator;
const initialSettings = { ...currentSettings() } as P2PSyncSetting; const initialSettings = { ...currentSettings() } as P2PSyncSetting;
let settings = $state<P2PSyncSetting>(initialSettings); let settings = $state<P2PSyncSetting>(initialSettings);
@@ -146,7 +144,7 @@
replicatorInfo = status; replicatorInfo = status;
}); });
applyLoadSettings(currentSettings(), true); applyLoadSettings(currentSettings(), true);
events.emitEvent(EVENT_REQUEST_STATUS); host.p2p.diagnostics.requestStatus();
return () => { return () => {
r(); r();
rx(); rx();
@@ -223,16 +221,16 @@
} }
async function openServer() { async function openServer() {
await currentReplicator().open(); await host.p2p.transportLifecycle.connect();
} }
async function closeServer() { async function closeServer() {
await currentReplicator().close(); await host.p2p.transportLifecycle.disconnect();
} }
function startBroadcasting() { function startBroadcasting() {
currentReplicator().enableBroadcastChanges(); host.p2p.changeRelay.enableBroadcastChanges();
} }
function stopBroadcasting() { function stopBroadcasting() {
currentReplicator().disableBroadcastChanges(); host.p2p.changeRelay.disableBroadcastChanges();
} }
const initialDialogStatusKey = `p2p-dialog-status`; const initialDialogStatusKey = `p2p-dialog-status`;
@@ -1,12 +1,20 @@
import type { RequiredServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import type { RequiredServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon"; import type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
export type P2PReplicatorHandle = Pick<UseP2PReplicatorResult, "replicator">; /**
* The shared pane only needs the contracts which represent its visible
* actions. In particular, it must not receive the compatibility Replicator
* facade, whose lifecycle methods can bypass the stable P2P service owner.
*/
export type P2PReplicatorPaneP2P = Pick<
P2PServiceViews,
"transportLifecycle" | "peerDirectory" | "peerAdmission" | "targetedTransfer" | "changeRelay" | "diagnostics"
>;
/** Host capabilities consumed by the shared P2P pane. */ /** Host capabilities consumed by the shared P2P pane. */
export interface P2PReplicatorPaneHost { export interface P2PReplicatorPaneHost {
readonly services: RequiredServices<"API" | "config" | "setting" | "vault">; readonly services: RequiredServices<"API" | "config" | "setting" | "vault">;
readonly p2p: P2PReplicatorHandle; readonly p2p: P2PReplicatorPaneP2P;
readonly showPeerMenu?: (peer: PeerStatus, event: MouseEvent) => void; readonly showPeerMenu?: (peer: PeerStatus, event: MouseEvent) => void;
} }
@@ -8,7 +8,7 @@ import { LOG_LEVEL_NOTICE, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon"; import type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
import type { P2PPaneParams } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult"; import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
export const VIEW_TYPE_P2P = "p2p-replicator"; export const VIEW_TYPE_P2P = "p2p-replicator";
function addToList(item: string, list: string) { function addToList(item: string, list: string) {
@@ -31,7 +31,7 @@ function removeFromList(item: string, list: string) {
export class P2PReplicatorPaneView extends SvelteItemView { export class P2PReplicatorPaneView extends SvelteItemView {
core: LiveSyncBaseCore; core: LiveSyncBaseCore;
private _p2pResult: P2PPaneParams; private _p2p: P2PServiceViews;
override icon = "waypoints"; override icon = "waypoints";
title: string = ""; title: string = "";
override navigation = false; override navigation = false;
@@ -39,21 +39,18 @@ export class P2PReplicatorPaneView extends SvelteItemView {
override getIcon(): string { override getIcon(): string {
return "waypoints"; return "waypoints";
} }
get replicator() {
return this._p2pResult.replicator;
}
async replicateFrom(peer: PeerStatus) { async replicateFrom(peer: PeerStatus) {
await this.replicator.replicateFrom(peer.peerId); await this._p2p.targetedTransfer.pullFromPeer(peer.peerId);
} }
async replicateTo(peer: PeerStatus) { async replicateTo(peer: PeerStatus) {
await this.replicator.requestSynchroniseToPeer(peer.peerId); await this._p2p.targetedTransfer.requestPushToPeer(peer.peerId);
} }
async getRemoteConfig(peer: PeerStatus) { async getRemoteConfig(peer: PeerStatus) {
Logger( Logger(
`Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`, `Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`,
LOG_LEVEL_NOTICE LOG_LEVEL_NOTICE
); );
const remoteConfig = await this.replicator.getRemoteConfig(peer.peerId); const remoteConfig = await this._p2p.configurationExchange.getRemoteConfiguration(peer.peerId);
if (remoteConfig) { if (remoteConfig) {
Logger(`Remote config for ${peer.name} is retrieved successfully`); Logger(`Remote config for ${peer.name} is retrieved successfully`);
const DROP = "Yes, and drop local database"; const DROP = "Yes, and drop local database";
@@ -122,10 +119,10 @@ And you can also drop the local database to rebuild from the remote device.`,
await this.core.services.setting.applyPartial(currentSetting, true); await this.core.services.setting.applyPartial(currentSetting, true);
} }
m?: Menu; m?: Menu;
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams) { constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2p: P2PServiceViews) {
super(leaf); super(leaf);
this.core = core; this.core = core;
this._p2pResult = p2pResult; this._p2p = p2p;
} }
private showPeerMenu(peer: PeerStatus, event: MouseEvent): void { private showPeerMenu(peer: PeerStatus, event: MouseEvent): void {
@@ -187,7 +184,7 @@ And you can also drop the local database to rebuild from the remote device.`,
props: { props: {
host: { host: {
services: this.core.services, services: this.core.services,
p2p: this._p2pResult, p2p: this._p2p,
showPeerMenu: (peer: PeerStatus, event: MouseEvent) => this.showPeerMenu(peer, event), showPeerMenu: (peer: PeerStatus, event: MouseEvent) => this.showPeerMenu(peer, event),
}, },
}, },
@@ -9,19 +9,19 @@
EVENT_P2P_REPLICATOR_STATUS, EVENT_P2P_REPLICATOR_STATUS,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer"; } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import type { P2PReplicatorStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator"; import type { P2PReplicatorStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
import { extractP2PRoomSuffix } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { extractP2PRoomSuffix } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { $msg as translateMessage } from "@/common/translation"; import { $msg as translateMessage } from "@/common/translation";
interface Props { interface Props {
getLiveSyncReplicator: () => LiveSyncTrysteroReplicator; p2p: P2PServiceViews;
showBroadcastToggle?: boolean; showBroadcastToggle?: boolean;
core?: LiveSyncBaseCore; core?: LiveSyncBaseCore;
} }
let { getLiveSyncReplicator, showBroadcastToggle = true, core }: Props = $props(); let { p2p, showBroadcastToggle = true, core }: Props = $props();
let serverInfo = $state<P2PServerInfo | undefined>(undefined); let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let replicatorStatus = $state<P2PReplicatorStatus | undefined>(undefined); let replicatorStatus = $state<P2PReplicatorStatus | undefined>(undefined);
// Later setting changes arrive through EVENT_SETTING_SAVED; these values only seed local state at mount time. // Later setting changes arrive through EVENT_SETTING_SAVED; these values only seed local state at mount time.
@@ -31,25 +31,25 @@
let useDiagRTC = $state<boolean>(initialSettings?.P2P_useDiagRTC ?? false); let useDiagRTC = $state<boolean>(initialSettings?.P2P_useDiagRTC ?? false);
async function requestServerStatus() { async function requestServerStatus() {
await Promise.resolve(getLiveSyncReplicator().requestStatus()); p2p.diagnostics.requestStatus();
eventHub.emitEvent(EVENT_REQUEST_STATUS); eventHub.emitEvent(EVENT_REQUEST_STATUS);
} }
async function onOpenConnection() { async function onOpenConnection() {
await getLiveSyncReplicator().makeSureOpened(); await p2p.transportLifecycle.connect();
await requestServerStatus(); await requestServerStatus();
} }
async function onDisconnect() { async function onDisconnect() {
await getLiveSyncReplicator().close(); await p2p.transportLifecycle.disconnect();
await requestServerStatus(); await requestServerStatus();
} }
function toggleBroadcast() { function toggleBroadcast() {
if (replicatorStatus?.isBroadcasting) { if (replicatorStatus?.isBroadcasting) {
getLiveSyncReplicator().disableBroadcastChanges(); p2p.changeRelay.disableBroadcastChanges();
} else { } else {
getLiveSyncReplicator().enableBroadcastChanges(); p2p.changeRelay.enableBroadcastChanges();
} }
} }
@@ -8,7 +8,7 @@
EVENT_P2P_REPLICATOR_PROGRESS, EVENT_P2P_REPLICATOR_PROGRESS,
type P2PServerInfo, type P2PServerInfo,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer"; } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import type { P2PReplicatorStatus, P2PReplicationReport } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator"; import type { P2PReplicatorStatus, P2PReplicationReport } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
import { delay, fireAndForget } from "octagonal-wheels/promises"; import { delay, fireAndForget } from "octagonal-wheels/promises";
import P2PServerStatusCard from "./P2PServerStatusCard.svelte"; import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
@@ -33,11 +33,11 @@
} from "./p2pPeerSettings"; } from "./p2pPeerSettings";
interface Props { interface Props {
getLiveSyncReplicator: () => LiveSyncTrysteroReplicator; p2p: P2PServiceViews;
core: LiveSyncBaseCore; core: LiveSyncBaseCore;
} }
let { getLiveSyncReplicator, core }: Props = $props(); let { p2p, core }: Props = $props();
let serverInfo = $state<P2PServerInfo | undefined>(undefined); let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined); let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
let decidingPeerId = $state<string | null>(null); let decidingPeerId = $state<string | null>(null);
@@ -121,7 +121,7 @@
} }
async function requestServerStatus() { async function requestServerStatus() {
await getLiveSyncReplicator().requestStatus(); p2p.diagnostics.requestStatus();
eventHub.emitEvent(EVENT_REQUEST_STATUS); eventHub.emitEvent(EVENT_REQUEST_STATUS);
} }
@@ -296,7 +296,7 @@
) { ) {
decidingPeerId = peer.peerId; decidingPeerId = peer.peerId;
try { try {
await getLiveSyncReplicator().makeDecision({ await p2p.peerAdmission.makeDecision({
peerId: peer.peerId, peerId: peer.peerId,
name: peer.name, name: peer.name,
decision, decision,
@@ -311,7 +311,7 @@
async function revokeDecision(peer: P2PServerInfo["knownAdvertisements"][number]) { async function revokeDecision(peer: P2PServerInfo["knownAdvertisements"][number]) {
decidingPeerId = peer.peerId; decidingPeerId = peer.peerId;
try { try {
await getLiveSyncReplicator().revokeDecision({ await p2p.peerAdmission.revokeDecision({
peerId: peer.peerId, peerId: peer.peerId,
name: peer.name, name: peer.name,
}); });
@@ -324,10 +324,7 @@
async function startReplication(peer: P2PServerInfo["knownAdvertisements"][number]) { async function startReplication(peer: P2PServerInfo["knownAdvertisements"][number]) {
replicatingPeerId = peer.peerId; replicatingPeerId = peer.peerId;
try { try {
const pullResult = await getLiveSyncReplicator().replicateFrom(peer.peerId, true); await p2p.targetedTransfer.synchroniseWithPeer(peer.peerId, true);
if (pullResult?.ok) {
await getLiveSyncReplicator().requestSynchroniseToPeer(peer.peerId);
}
await requestServerStatus(); await requestServerStatus();
} finally { } finally {
replicatingPeerId = null; replicatingPeerId = null;
@@ -347,9 +344,9 @@
return; return;
} }
if (isWatching(peerId)) { if (isWatching(peerId)) {
getLiveSyncReplicator().unwatchPeer(peerId); p2p.changeRelay.unwatchPeer(peerId);
} else { } else {
getLiveSyncReplicator().watchPeer(peerId); p2p.changeRelay.watchPeer(peerId);
} }
} }
@@ -455,7 +452,7 @@
</p> </p>
{/if} {/if}
<P2PServerStatusCard {getLiveSyncReplicator} {core} /> <P2PServerStatusCard {p2p} {core} />
<div class="peers-section"> <div class="peers-section">
<div class="peers-header"> <div class="peers-header">
@@ -2,21 +2,21 @@ import { WorkspaceLeaf } from "@/deps.ts";
import { mount } from "svelte"; import { mount } from "svelte";
import { SvelteItemView } from "@/common/SvelteItemView.ts"; import { SvelteItemView } from "@/common/SvelteItemView.ts";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
import type { P2PPaneParams } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult"; import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import P2PServerStatusPane from "./P2PServerStatusPane.svelte"; import P2PServerStatusPane from "./P2PServerStatusPane.svelte";
export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status"; export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status";
export class P2PServerStatusPaneView extends SvelteItemView { export class P2PServerStatusPaneView extends SvelteItemView {
core: LiveSyncBaseCore; core: LiveSyncBaseCore;
private _p2pResult: P2PPaneParams; private readonly p2p: P2PServiceViews;
override icon = "waypoints"; override icon = "waypoints";
override navigation = false; override navigation = false;
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams) { constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2p: P2PServiceViews) {
super(leaf); super(leaf);
this.core = core; this.core = core;
this._p2pResult = p2pResult; this.p2p = p2p;
} }
override getIcon(): string { override getIcon(): string {
@@ -35,7 +35,7 @@ export class P2PServerStatusPaneView extends SvelteItemView {
return mount(P2PServerStatusPane, { return mount(P2PServerStatusPane, {
target, target,
props: { props: {
getLiveSyncReplicator: () => this._p2pResult.replicator, p2p: this.p2p,
core: this.core, core: this.core,
}, },
}); });
@@ -1,17 +1,16 @@
<script lang="ts"> <script lang="ts">
import { AcceptedStatus, type PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon"; import { AcceptedStatus, type PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import type { P2PReplicatorHandle } from "./P2PReplicatorPaneHost"; import type { P2PReplicatorPaneP2P } from "./P2PReplicatorPaneHost";
import { $msg as translateMessage } from "@/common/translation"; import { $msg as translateMessage } from "@/common/translation";
interface Props { interface Props {
peerStatus: PeerStatus; peerStatus: PeerStatus;
p2p: P2PReplicatorHandle; p2p: P2PReplicatorPaneP2P;
showPeerMenu?: (peer: PeerStatus, event: MouseEvent) => void; showPeerMenu?: (peer: PeerStatus, event: MouseEvent) => void;
} }
let { peerStatus, p2p, showPeerMenu }: Props = $props(); let { peerStatus, p2p, showPeerMenu }: Props = $props();
let peer = $derived(peerStatus); let peer = $derived(peerStatus);
const currentReplicator = () => p2p.replicator;
function select<T extends PropertyKey, U, V = undefined>( function select<T extends PropertyKey, U, V = undefined>(
d: T, d: T,
@@ -72,7 +71,7 @@
let isNew = $derived.by(() => peer.accepted === AcceptedStatus.UNKNOWN); let isNew = $derived.by(() => peer.accepted === AcceptedStatus.UNKNOWN);
function makeDecision(isAccepted: boolean, isTemporary: boolean) { function makeDecision(isAccepted: boolean, isTemporary: boolean) {
currentReplicator().makeDecision({ void p2p.peerAdmission.makeDecision({
peerId: peer.peerId, peerId: peer.peerId,
name: peer.name, name: peer.name,
decision: isAccepted, decision: isAccepted,
@@ -80,7 +79,7 @@
}); });
} }
function revokeDecision() { function revokeDecision() {
currentReplicator().revokeDecision({ void p2p.peerAdmission.revokeDecision({
peerId: peer.peerId, peerId: peer.peerId,
name: peer.name, name: peer.name,
}); });
@@ -99,14 +98,14 @@
return attrs; return attrs;
}); });
function startWatching() { function startWatching() {
currentReplicator().watchPeer(peer.peerId); p2p.changeRelay.watchPeer(peer.peerId);
} }
function stopWatching() { function stopWatching() {
currentReplicator().unwatchPeer(peer.peerId); p2p.changeRelay.unwatchPeer(peer.peerId);
} }
function sync() { function sync() {
void currentReplicator().sync(peer.peerId, false); void p2p.targetedTransfer.synchroniseWithPeer(peer.peerId, false);
} }
function moreMenu(evt: MouseEvent) { function moreMenu(evt: MouseEvent) {
+1 -1
View File
@@ -369,7 +369,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
serviceModules: {}, serviceModules: {},
}); });
services.replication.onBeforeReplicate.addHandler(isOnlineAndCanReplicateWithHost, 10); services.replication.onBeforeReplicate.addHandler(isOnlineAndCanReplicateWithHost, 10);
services.replication.onBeforeReplicate.addHandler(canReplicateWithPBKDF2WithHost, 20); services.replication.onPrepareCentralRemoteReplication.addHandler(canReplicateWithPBKDF2WithHost);
// <-- End of handlers that can be separated. // <-- End of handlers that can be separated.
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this), 100); services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this), 100);
services.replication.onReplicationFailed.addHandler(this.onReplicationFailed.bind(this)); services.replication.onReplicationFailed.addHandler(this.onReplicationFailed.bind(this));
+68 -9
View File
@@ -19,12 +19,7 @@ import { ModuleReplicator } from "./ModuleReplicator";
describe("ModuleReplicator", () => { describe("ModuleReplicator", () => {
it("refreshes the remote Security Seed before replication", async () => { it("refreshes the remote Security Seed before replication", async () => {
const ensurePBKDF2Salt = vi.fn(async () => true); const ensurePBKDF2Salt = vi.fn(async () => true);
let beforeReplicate: ((showMessage: boolean) => Promise<boolean>) | undefined; let prepareCentralRemoteReplication: ((showMessage: boolean) => Promise<boolean>) | undefined;
const addHandler = vi.fn((handler: (showMessage: boolean) => Promise<boolean>, priority?: number) => {
if (priority === 20) {
beforeReplicate = handler;
}
});
const services = { const services = {
API: { isOnline: true }, API: { isOnline: true },
replicator: { replicator: {
@@ -36,7 +31,12 @@ describe("ModuleReplicator", () => {
appLifecycle: { onSettingLoaded: { addHandler: vi.fn() } }, appLifecycle: { onSettingLoaded: { addHandler: vi.fn() } },
replication: { replication: {
parseSynchroniseResult: { addHandler: vi.fn() }, parseSynchroniseResult: { addHandler: vi.fn() },
onBeforeReplicate: { addHandler }, onBeforeReplicate: { addHandler: vi.fn() },
onPrepareCentralRemoteReplication: {
addHandler: vi.fn((handler: (showMessage: boolean) => Promise<boolean>) => {
prepareCentralRemoteReplication = handler;
}),
},
onReplicationFailed: { addHandler: vi.fn() }, onReplicationFailed: { addHandler: vi.fn() },
}, },
}; };
@@ -54,13 +54,72 @@ describe("ModuleReplicator", () => {
}; };
ModuleReplicator.prototype.onBindFunction.call(module, {} as never, services as never); ModuleReplicator.prototype.onBindFunction.call(module, {} as never, services as never);
expect(beforeReplicate).toBeDefined(); expect(prepareCentralRemoteReplication).toBeDefined();
await beforeReplicate!(false); await prepareCentralRemoteReplication!(false);
expect(ensurePBKDF2Salt).toHaveBeenCalledWith({}, false, false); expect(ensurePBKDF2Salt).toHaveBeenCalledWith({}, false, false);
}); });
it("keeps online and general pre-replication handlers for P2P while skipping central-remote Security Seed preparation", async () => {
const ensurePBKDF2Salt = vi.fn(async () => true);
const handlers = new Map<number, (...args: unknown[]) => Promise<boolean | void>>();
const centralRemoteHandlers: Array<(...args: unknown[]) => Promise<boolean | void>> = [];
const addHandler = vi.fn((handler: (...args: unknown[]) => Promise<boolean | void>, priority?: number) => {
handlers.set(priority ?? 0, handler);
});
const services = {
API: { isOnline: true },
replicator: {
onReplicatorInitialised: { addHandler: vi.fn() },
getActiveReplicator: () => ({ ensurePBKDF2Salt }),
},
setting: { currentSettings: () => ({}) },
databaseEvents: { onDatabaseInitialised: { addHandler: vi.fn() } },
appLifecycle: { onSettingLoaded: { addHandler: vi.fn() } },
replication: {
parseSynchroniseResult: { addHandler: vi.fn() },
onBeforeReplicate: { addHandler },
onPrepareCentralRemoteReplication: {
addHandler: vi.fn((handler: (...args: unknown[]) => Promise<boolean | void>) => {
centralRemoteHandlers.push(handler);
}),
},
onReplicationFailed: { addHandler: vi.fn() },
},
};
const generalBeforeReplicate = vi.fn(async () => true);
const module = {
_unresolvedErrorManager: {
showError: vi.fn(),
clearError: vi.fn(),
},
_onReplicatorInitialised: vi.fn(),
_everyOnDatabaseInitialized: vi.fn(),
_everyOnloadAfterLoadSettings: vi.fn(),
_parseReplicationResult: vi.fn(),
_everyBeforeReplicate: generalBeforeReplicate,
onReplicationFailed: vi.fn(),
};
ModuleReplicator.prototype.onBindFunction.call(module, {} as never, services as never);
const online = handlers.get(10);
const securitySeed = centralRemoteHandlers[0];
const general = handlers.get(100);
expect(online).toBeDefined();
expect(securitySeed).toBeDefined();
expect(general).toBeDefined();
await expect(online!(false)).resolves.toBe(true);
await expect(general!(false)).resolves.toBe(true);
expect(generalBeforeReplicate).toHaveBeenCalledOnce();
expect(ensurePBKDF2Salt).not.toHaveBeenCalled();
await expect(securitySeed!(false)).resolves.toBe(true);
expect(ensurePBKDF2Salt).toHaveBeenCalledOnce();
});
it("reprocesses stored documents when the normal-file target filters change", async () => { it("reprocesses stored documents when the normal-file target filters change", async () => {
eventHub.offAll(); eventHub.offAll();
const settings = { const settings = {
+9 -6
View File
@@ -96,10 +96,10 @@ export function useP2PReplicatorUI(
}; };
const statusFactory = (leaf: WorkspaceLeaf) => { const statusFactory = (leaf: WorkspaceLeaf) => {
return new P2PServerStatusPaneView(leaf, core, p2pParams); return new P2PServerStatusPaneView(leaf, core, replicator);
}; };
const legacyStatusFactory = (leaf: WorkspaceLeaf) => { const legacyStatusFactory = (leaf: WorkspaceLeaf) => {
return new LegacyP2PStatusPaneView(leaf, core, p2pParams); return new LegacyP2PStatusPaneView(leaf, core, replicator);
}; };
const openStatusPane = () => { const openStatusPane = () => {
if (api.showWindowOnRight) { if (api.showWindowOnRight) {
@@ -167,7 +167,7 @@ export function useP2PReplicatorUI(
const isAvailable = const isAvailable =
hasP2PConfiguration(settings) && hasP2PConfiguration(settings) &&
settings.remoteType !== REMOTE_P2P && settings.remoteType !== REMOTE_P2P &&
(replicator.replicator?.server?.isServing ?? false); replicator.transportLifecycle.isConnected;
if (!isAvailable) return false; if (!isAvailable) return false;
if (!isChecking) { if (!isChecking) {
runOpenReplication(); runOpenReplication();
@@ -183,7 +183,7 @@ export function useP2PReplicatorUI(
const isAvailable = const isAvailable =
hasP2PConfiguration(settings) && hasP2PConfiguration(settings) &&
settings.remoteType !== REMOTE_P2P && settings.remoteType !== REMOTE_P2P &&
(replicator.replicator?.server?.isServing ?? false); replicator.transportLifecycle.isConnected;
if (!isAvailable) return false; if (!isAvailable) return false;
if (!isChecking) { if (!isChecking) {
runOpenReplication(); runOpenReplication();
@@ -198,10 +198,13 @@ export function useP2PReplicatorUI(
checkCallback: (isChecking: boolean) => { checkCallback: (isChecking: boolean) => {
const isAvailable = const isAvailable =
hasP2PConfiguration(host.services.setting.currentSettings()) && hasP2PConfiguration(host.services.setting.currentSettings()) &&
(replicator.replicator?.server?.isServing ?? false); replicator.transportLifecycle.isConnected;
if (!isAvailable) return false; if (!isAvailable) return false;
if (!isChecking) { if (!isChecking) {
void replicator.replicator?.replicateFromCommand(true); void host.services.replicator.runFiniteReplicationActivity(
() => replicator.targetedTransfer.synchroniseConfiguredTargets(),
{ label: "replication" }
);
} }
return true; return true;
}, },
@@ -95,6 +95,7 @@ describe("useP2PReplicatorUI commands", () => {
}, },
} as any; } as any;
const p2p = { const p2p = {
transportLifecycle: { isConnected: true },
replicator: { replicator: {
server: { isServing: true }, server: { isServing: true },
openReplication, openReplication,
@@ -209,6 +210,7 @@ describe("useP2PReplicatorUI commands", () => {
remoteType: "COUCHDB", remoteType: "COUCHDB",
remoteConfigurations: {}, remoteConfigurations: {},
}; };
const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task());
const host = { const host = {
services: { services: {
context: createServiceContext(), context: createServiceContext(),
@@ -232,14 +234,18 @@ describe("useP2PReplicatorUI commands", () => {
currentSettings: vi.fn(() => settings), currentSettings: vi.fn(() => settings),
onSettingSaved: { addHandler: vi.fn() }, onSettingSaved: { addHandler: vi.fn() },
}, },
replicator: { runFiniteReplicationActivity: vi.fn() }, replicator: { runFiniteReplicationActivity },
}, },
} as any; } as any;
const replicateFromCommand = vi.fn();
const synchroniseConfiguredTargets = vi.fn(async () => ({ status: "completed" }));
const p2p = { const p2p = {
transportLifecycle: { isConnected: true },
targetedTransfer: { synchroniseConfiguredTargets },
replicator: { replicator: {
server: { isServing: true }, server: { isServing: true },
openReplication: vi.fn(), openReplication: vi.fn(),
replicateFromCommand: vi.fn(), replicateFromCommand,
}, },
} as any; } as any;
@@ -274,6 +280,13 @@ describe("useP2PReplicatorUI commands", () => {
]) { ]) {
expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(true); expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(true);
} }
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",
});
}); });
it("does not open the P2P status pane automatically when the workspace becomes ready", async () => { it("does not open the P2P status pane automatically when the workspace becomes ready", async () => {
@@ -0,0 +1,57 @@
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { beforeEach, describe, expect, it, vi } from "vitest";
const runtimeMocks = vi.hoisted(() => {
const connect = vi.fn().mockResolvedValue(undefined);
const makeSureOpened = vi.fn().mockResolvedValue(undefined);
const start = vi.fn().mockResolvedValue(undefined);
const shutdown = vi.fn().mockResolvedValue(undefined);
const removeStatusListener = vi.fn();
const onEvent = vi.fn(() => removeStatusListener);
const WebPeerRuntime = vi.fn(function () {
return {
events: { onEvent },
p2p: {
transportLifecycle: { connect },
},
currentReplicator: { makeSureOpened },
start,
shutdown,
};
});
return {
WebPeerRuntime,
connect,
makeSureOpened,
onEvent,
removeStatusListener,
shutdown,
start,
};
});
vi.mock("@/apps/webpeer/src/WebPeerRuntime", () => ({
WebPeerRuntime: runtimeMocks.WebPeerRuntime,
}));
import { P2PCheckSession } from "@/apps/webpeer/src/P2PCheckSession";
describe("P2P connection-check session", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("opens an explicit check through the P2P transport lifecycle", async () => {
const session = new P2PCheckSession();
await session.start({} as ObsidianLiveSyncSettings, "browser-check", vi.fn());
expect(runtimeMocks.start).toHaveBeenCalledOnce();
expect(runtimeMocks.connect).toHaveBeenCalledOnce();
expect(runtimeMocks.makeSureOpened).not.toHaveBeenCalled();
await session.stop();
expect(runtimeMocks.shutdown).toHaveBeenCalledOnce();
});
});
+29 -1
View File
@@ -34,7 +34,7 @@ describe("WebPeer runtime composition", () => {
expect(runtime.context).toBe(context); expect(runtime.context).toBe(context);
expect(runtime.services.context).toBe(context); expect(runtime.services.context).toBe(context);
expect(runtime.events).toBe(context.events); expect(runtime.events).toBe(context.events);
expect(runtime.currentReplicator).toBe(runtime.p2p.replicator); expect(runtime.paneHost.p2p.transportLifecycle).toBe(runtime.p2p.transportLifecycle);
expect(runtime.paneHost.services).toBe(runtime.services); expect(runtime.paneHost.services).toBe(runtime.services);
expect(runtime.paneHost.p2p).toBe(runtime.p2p); expect(runtime.paneHost.p2p).toBe(runtime.p2p);
expect(runtime.paneHost.showPeerMenu).toBeTypeOf("function"); expect(runtime.paneHost.showPeerMenu).toBeTypeOf("function");
@@ -64,6 +64,34 @@ describe("WebPeer runtime composition", () => {
expect(layoutReady).toHaveBeenCalledOnce(); expect(layoutReady).toHaveBeenCalledOnce();
}); });
it("delegates automatic P2P startup to the resumed lifecycle handler", async () => {
vi.useFakeTimers();
try {
const runtime = new WebPeerRuntime({
store: createMemoryStore(),
});
vi.spyOn(runtime.services.setting, "loadSettings").mockResolvedValue(undefined);
vi.spyOn(runtime.services.setting, "currentSettings").mockReturnValue({
...DEFAULT_SETTINGS,
P2P_AutoStart: true,
P2P_Enabled: true,
});
vi.spyOn(runtime.services.database, "openDatabase").mockResolvedValue(true);
const onResumed = vi
.spyOn(runtime.services.appLifecycle, "onResumed")
.mockResolvedValue(true);
const open = vi.spyOn(runtime.p2p.transportLifecycle, "connect").mockResolvedValue(undefined);
await runtime.start();
expect(onResumed).toHaveBeenCalledOnce();
await vi.advanceTimersByTimeAsync(100);
expect(open).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
}
});
it("rejects start when the browser-local database cannot be opened", async () => { it("rejects start when the browser-local database cannot be opened", async () => {
const runtime = new WebPeerRuntime({ const runtime = new WebPeerRuntime({
store: createMemoryStore(), store: createMemoryStore(),