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 {
CAPABILITY_NOT_APPLICABLE,
CENTRAL_REMOTE_REPLICATION_READINESS,
defineReplicatorProviderDefinitions,
supportedOpenReplicationContinuous,
supportedOpenReplicationOneShot,
@@ -158,6 +159,7 @@ export class LiveSyncBaseCore<
[REMOTE_COUCHDB]: {
kind: REMOTE_COUCHDB,
diagnosticName: "CouchDB",
readiness: CENTRAL_REMOTE_REPLICATION_READINESS,
isConfigured: (settings) =>
settings.remoteType === REMOTE_COUCHDB &&
!!settings.couchDB_URI?.trim() &&
@@ -171,6 +173,7 @@ export class LiveSyncBaseCore<
[REMOTE_MINIO]: {
kind: REMOTE_MINIO,
diagnosticName: "Object Storage",
readiness: CENTRAL_REMOTE_REPLICATION_READINESS,
isConfigured: (settings) =>
settings.remoteType === REMOTE_MINIO && !!settings.endpoint?.trim() && !!settings.bucket?.trim(),
create: (_settings) => Promise.resolve(new LiveSyncJournalReplicator(this)),
+1 -1
View File
@@ -53,7 +53,7 @@ export class P2PCheckSession {
try {
await runtime.start();
await runtime.currentReplicator.makeSureOpened();
await runtime.p2p.transportLifecycle.connect();
} catch (error) {
await this.stop();
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 type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
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 { ServiceContext, type LiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context";
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import { unique } from "octagonal-wheels/collection";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
@@ -48,7 +47,7 @@ function removeFromList(item: string, list: string): string {
export class WebPeerRuntime {
readonly context: ServiceContext;
readonly services: LiveSyncBrowserServiceHub<ServiceContext>;
readonly p2p: UseP2PReplicatorResult;
readonly p2p: P2PServiceViews;
readonly p2pLogCollector: P2PLogCollector;
readonly paneHost: P2PReplicatorPaneHost;
@@ -87,10 +86,6 @@ export class WebPeerRuntime {
return this.context.events;
}
get currentReplicator(): LiveSyncTrysteroReplicator {
return this.p2p.replicator;
}
get settings(): P2PSyncSetting {
return this.services.setting.currentSettings();
}
@@ -119,9 +114,7 @@ export class WebPeerRuntime {
}
this.services.appLifecycle.markIsReady();
this.events.emitEvent(EVENT_LAYOUT_READY);
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
compatGlobal.setTimeout(() => void this.currentReplicator.open(), 100);
}
await this.services.appLifecycle.onResumed();
return this;
}
@@ -151,12 +144,12 @@ export class WebPeerRuntime {
this.menu = new Menu()
.addItem((item) =>
item.setTitle("📥 Only fetch").onClick(async () => {
await this.currentReplicator.replicateFrom(peer.peerId);
await this.p2p.targetedTransfer.pullFromPeer(peer.peerId);
})
)
.addItem((item) =>
item.setTitle("📤 Only send").onClick(async () => {
await this.currentReplicator.requestSynchroniseToPeer(peer.peerId);
await this.p2p.targetedTransfer.requestPushToPeer(peer.peerId);
})
)
.addSeparator()
@@ -1,7 +1,7 @@
import { App, Modal } from "@/deps.ts";
import P2POpenReplicationPane from "./P2POpenReplicationPane.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 = {
onSync: (peerId: string) => Promise<void>;
@@ -9,7 +9,7 @@ export type P2POpenReplicationModalCallback = {
};
export class P2POpenReplicationModal extends Modal {
liveSyncReplicator: LiveSyncTrysteroReplicator;
p2p: P2PServiceViews;
callback?: P2POpenReplicationModalCallback;
component?: ReturnType<typeof mount>;
showResult: boolean;
@@ -19,7 +19,7 @@ export class P2POpenReplicationModal extends Modal {
constructor(
app: App,
liveSyncReplicator: LiveSyncTrysteroReplicator,
p2p: P2PServiceViews,
callback?: P2POpenReplicationModalCallback,
showResult: boolean = false,
title: string = "P2P Replication",
@@ -27,7 +27,7 @@ export class P2POpenReplicationModal extends Modal {
rebuildMode: boolean = false
) {
super(app);
this.liveSyncReplicator = liveSyncReplicator;
this.p2p = p2p;
this.callback = callback;
this.showResult = showResult;
this.title = title;
@@ -57,7 +57,7 @@ export class P2POpenReplicationModal extends Modal {
this.component = mount(P2POpenReplicationPane, {
target: contentEl,
props: {
liveSyncReplicator: this.liveSyncReplicator,
p2p: this.p2p,
onSync: (peerId: string) => this.onSync(peerId),
onSyncAndClose: (peerId: string) => this.onSyncAndClose(peerId),
onClose: () => this.close(),
@@ -9,13 +9,13 @@
// 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 { 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 P2PServerStatusCard from "./P2PServerStatusCard.svelte";
import { $msg as translateMessage } from "@/common/translation";
interface Props {
liveSyncReplicator: LiveSyncTrysteroReplicator;
p2p: P2PServiceViews;
onSync: (_peerId: string) => Promise<void>;
onSyncAndClose: (_peerId: string) => Promise<void>;
onClose: () => void;
@@ -23,15 +23,14 @@
rebuildMode?: boolean;
}
let { onSync, onSyncAndClose, onClose, showResult, liveSyncReplicator, rebuildMode = false }: Props = $props();
const getLiveSyncReplicator = () => liveSyncReplicator;
let { onSync, onSyncAndClose, onClose, showResult, p2p, rebuildMode = false }: Props = $props();
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let syncingPeerId = $state<string | null>(null);
const logLevel = $derived(showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
async function requestServerStatus() {
await liveSyncReplicator.requestStatus();
p2p.diagnostics.requestStatus();
eventHub.emitEvent(EVENT_REQUEST_STATUS);
}
onMount(() => {
@@ -73,7 +72,7 @@
async function disconnect() {
try {
await liveSyncReplicator.close();
await p2p.transportLifecycle.disconnect();
Logger("Signalling connection closed.", logLevel);
} catch (e) {
Logger(`Failed to close signalling connection: ${e instanceof Error ? e.message : String(e)}`, logLevel);
@@ -100,7 +99,7 @@
</script>
<div class="p2p-container">
<P2PServerStatusCard {getLiveSyncReplicator} showBroadcastToggle={false} />
<P2PServerStatusCard {p2p} showBroadcastToggle={false} />
<div class="peers-section">
<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 { 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 { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
import { P2POpenReplicationModal } from "./P2POpenReplicationModal";
/**
@@ -15,8 +16,8 @@ import { P2POpenReplicationModal } from "./P2POpenReplicationModal";
*/
export function createOpenReplicationUI(
app: App
): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise<boolean | void> {
return (replicator: LiveSyncTrysteroReplicator) =>
): (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void> {
return (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) =>
(showResult: boolean): Promise<boolean | void> => {
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
return new Promise<boolean | void>((resolve) => {
@@ -57,7 +58,7 @@ export function createOpenReplicationUI(
};
const modal = new P2POpenReplicationModal(
app,
replicator,
p2p,
{
onSync: (peerId: string) => synchronise(peerId, false),
onSyncAndClose: (peerId: string) => synchronise(peerId, true),
@@ -85,8 +86,8 @@ export function createOpenReplicationUI(
*/
export function createOpenRebuildUI(
app: App
): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise<boolean | void> {
return (replicator: LiveSyncTrysteroReplicator) =>
): (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void> {
return (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) =>
(showResult: boolean): Promise<boolean | void> => {
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
return new Promise<boolean | void>((resolve) => {
@@ -132,7 +133,7 @@ export function createOpenRebuildUI(
const modal = new P2POpenReplicationModal(
app,
replicator,
p2p,
{
onSync: doRebuild,
onSyncAndClose: doRebuild,
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const modalState = vi.hoisted(() => ({
instances: [] as Array<{
p2p: unknown;
callback: {
onSync: (peerId: string) => Promise<void>;
onSyncAndClose: (peerId: string) => Promise<void>;
@@ -15,18 +16,20 @@ vi.mock("@/deps.ts", () => ({ App: class {} }));
vi.mock("./P2POpenReplicationModal", () => ({
P2POpenReplicationModal: class {
p2p;
callback;
onClosed;
open = vi.fn();
constructor(
_app: unknown,
_replicator: unknown,
p2p: unknown,
callback: (typeof modalState.instances)[number]["callback"],
_showResult: boolean,
_title?: string,
onClosed?: () => void
) {
this.p2p = p2p;
this.callback = callback;
this.onClosed = onClosed;
modalState.instances.push(this);
@@ -46,15 +49,21 @@ function createReplicator() {
} as any;
}
function createP2PServiceViews() {
return { transportLifecycle: {}, diagnostics: {} } as any;
}
describe("createOpenReplicationUI", () => {
beforeEach(() => {
modalState.instances.length = 0;
});
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];
expect(modal.p2p).toBe(p2p);
expect(modal.onClosed).toBeTypeOf("function");
modal.onClosed?.();
@@ -63,7 +72,7 @@ describe("createOpenReplicationUI", () => {
it("keeps repeated synchronisation inside the session boundary until the modal closes", async () => {
const replicator = createReplicator();
const session = createOpenReplicationUI({} as any)(replicator)(true);
const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0];
let settled = false;
void session.finally(() => {
@@ -91,7 +100,7 @@ describe("createOpenReplicationUI", () => {
finishPull = resolve;
})
);
const session = createOpenReplicationUI({} as any)(replicator)(true);
const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0];
let settled = false;
void session.finally(() => {
@@ -111,7 +120,7 @@ describe("createOpenReplicationUI", () => {
it("closes the P2P connection after a successful sync-and-close action", async () => {
const replicator = createReplicator();
const session = createOpenReplicationUI({} as any)(replicator)(true);
const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0];
await modal.callback.onSyncAndClose("peer-a");
@@ -143,7 +152,7 @@ describe("createOpenRebuildUI", () => {
finishPull = resolve;
})
);
const session = createOpenRebuildUI({} as any)(replicator)(true);
const session = createOpenRebuildUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0];
let settled = false;
void session.finally(() => {
@@ -166,7 +175,7 @@ describe("createOpenRebuildUI", () => {
it("does not complete Fetch when the rebuild dialogue closes without selecting a peer", async () => {
const replicator = createReplicator();
const session = createOpenRebuildUI({} as any)(replicator)(true);
const session = createOpenRebuildUI({} as any)(replicator, createP2PServiceViews())(true);
const modal = modalState.instances[0];
modal.onClosed?.();
@@ -13,7 +13,6 @@
type PeerInfo,
type P2PServerInfo,
EVENT_SERVER_STATUS,
EVENT_REQUEST_STATUS,
EVENT_P2P_REPLICATOR_STATUS,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
import type { P2PReplicatorStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
@@ -29,7 +28,6 @@
let services = $derived(host.services);
let events = $derived(services.context.events);
const currentSettings = () => services.setting.currentSettings() as P2PSyncSetting;
const currentReplicator = () => host.p2p.replicator;
const initialSettings = { ...currentSettings() } as P2PSyncSetting;
let settings = $state<P2PSyncSetting>(initialSettings);
@@ -146,7 +144,7 @@
replicatorInfo = status;
});
applyLoadSettings(currentSettings(), true);
events.emitEvent(EVENT_REQUEST_STATUS);
host.p2p.diagnostics.requestStatus();
return () => {
r();
rx();
@@ -223,16 +221,16 @@
}
async function openServer() {
await currentReplicator().open();
await host.p2p.transportLifecycle.connect();
}
async function closeServer() {
await currentReplicator().close();
await host.p2p.transportLifecycle.disconnect();
}
function startBroadcasting() {
currentReplicator().enableBroadcastChanges();
host.p2p.changeRelay.enableBroadcastChanges();
}
function stopBroadcasting() {
currentReplicator().disableBroadcastChanges();
host.p2p.changeRelay.disableBroadcastChanges();
}
const initialDialogStatusKey = `p2p-dialog-status`;
@@ -1,12 +1,20 @@
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 { 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. */
export interface P2PReplicatorPaneHost {
readonly services: RequiredServices<"API" | "config" | "setting" | "vault">;
readonly p2p: P2PReplicatorHandle;
readonly p2p: P2PReplicatorPaneP2P;
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 type { PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
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";
function addToList(item: string, list: string) {
@@ -31,7 +31,7 @@ function removeFromList(item: string, list: string) {
export class P2PReplicatorPaneView extends SvelteItemView {
core: LiveSyncBaseCore;
private _p2pResult: P2PPaneParams;
private _p2p: P2PServiceViews;
override icon = "waypoints";
title: string = "";
override navigation = false;
@@ -39,21 +39,18 @@ export class P2PReplicatorPaneView extends SvelteItemView {
override getIcon(): string {
return "waypoints";
}
get replicator() {
return this._p2pResult.replicator;
}
async replicateFrom(peer: PeerStatus) {
await this.replicator.replicateFrom(peer.peerId);
await this._p2p.targetedTransfer.pullFromPeer(peer.peerId);
}
async replicateTo(peer: PeerStatus) {
await this.replicator.requestSynchroniseToPeer(peer.peerId);
await this._p2p.targetedTransfer.requestPushToPeer(peer.peerId);
}
async getRemoteConfig(peer: PeerStatus) {
Logger(
`Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`,
LOG_LEVEL_NOTICE
);
const remoteConfig = await this.replicator.getRemoteConfig(peer.peerId);
const remoteConfig = await this._p2p.configurationExchange.getRemoteConfiguration(peer.peerId);
if (remoteConfig) {
Logger(`Remote config for ${peer.name} is retrieved successfully`);
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);
}
m?: Menu;
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams) {
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2p: P2PServiceViews) {
super(leaf);
this.core = core;
this._p2pResult = p2pResult;
this._p2p = p2p;
}
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: {
host: {
services: this.core.services,
p2p: this._p2pResult,
p2p: this._p2p,
showPeerMenu: (peer: PeerStatus, event: MouseEvent) => this.showPeerMenu(peer, event),
},
},
@@ -9,19 +9,19 @@
EVENT_P2P_REPLICATOR_STATUS,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
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 { extractP2PRoomSuffix } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { $msg as translateMessage } from "@/common/translation";
interface Props {
getLiveSyncReplicator: () => LiveSyncTrysteroReplicator;
p2p: P2PServiceViews;
showBroadcastToggle?: boolean;
core?: LiveSyncBaseCore;
}
let { getLiveSyncReplicator, showBroadcastToggle = true, core }: Props = $props();
let { p2p, showBroadcastToggle = true, core }: Props = $props();
let serverInfo = $state<P2PServerInfo | 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.
@@ -31,25 +31,25 @@
let useDiagRTC = $state<boolean>(initialSettings?.P2P_useDiagRTC ?? false);
async function requestServerStatus() {
await Promise.resolve(getLiveSyncReplicator().requestStatus());
p2p.diagnostics.requestStatus();
eventHub.emitEvent(EVENT_REQUEST_STATUS);
}
async function onOpenConnection() {
await getLiveSyncReplicator().makeSureOpened();
await p2p.transportLifecycle.connect();
await requestServerStatus();
}
async function onDisconnect() {
await getLiveSyncReplicator().close();
await p2p.transportLifecycle.disconnect();
await requestServerStatus();
}
function toggleBroadcast() {
if (replicatorStatus?.isBroadcasting) {
getLiveSyncReplicator().disableBroadcastChanges();
p2p.changeRelay.disableBroadcastChanges();
} else {
getLiveSyncReplicator().enableBroadcastChanges();
p2p.changeRelay.enableBroadcastChanges();
}
}
@@ -8,7 +8,7 @@
EVENT_P2P_REPLICATOR_PROGRESS,
type P2PServerInfo,
} 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 { delay, fireAndForget } from "octagonal-wheels/promises";
import P2PServerStatusCard from "./P2PServerStatusCard.svelte";
@@ -33,11 +33,11 @@
} from "./p2pPeerSettings";
interface Props {
getLiveSyncReplicator: () => LiveSyncTrysteroReplicator;
p2p: P2PServiceViews;
core: LiveSyncBaseCore;
}
let { getLiveSyncReplicator, core }: Props = $props();
let { p2p, core }: Props = $props();
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
let decidingPeerId = $state<string | null>(null);
@@ -121,7 +121,7 @@
}
async function requestServerStatus() {
await getLiveSyncReplicator().requestStatus();
p2p.diagnostics.requestStatus();
eventHub.emitEvent(EVENT_REQUEST_STATUS);
}
@@ -296,7 +296,7 @@
) {
decidingPeerId = peer.peerId;
try {
await getLiveSyncReplicator().makeDecision({
await p2p.peerAdmission.makeDecision({
peerId: peer.peerId,
name: peer.name,
decision,
@@ -311,7 +311,7 @@
async function revokeDecision(peer: P2PServerInfo["knownAdvertisements"][number]) {
decidingPeerId = peer.peerId;
try {
await getLiveSyncReplicator().revokeDecision({
await p2p.peerAdmission.revokeDecision({
peerId: peer.peerId,
name: peer.name,
});
@@ -324,10 +324,7 @@
async function startReplication(peer: P2PServerInfo["knownAdvertisements"][number]) {
replicatingPeerId = peer.peerId;
try {
const pullResult = await getLiveSyncReplicator().replicateFrom(peer.peerId, true);
if (pullResult?.ok) {
await getLiveSyncReplicator().requestSynchroniseToPeer(peer.peerId);
}
await p2p.targetedTransfer.synchroniseWithPeer(peer.peerId, true);
await requestServerStatus();
} finally {
replicatingPeerId = null;
@@ -347,9 +344,9 @@
return;
}
if (isWatching(peerId)) {
getLiveSyncReplicator().unwatchPeer(peerId);
p2p.changeRelay.unwatchPeer(peerId);
} else {
getLiveSyncReplicator().watchPeer(peerId);
p2p.changeRelay.watchPeer(peerId);
}
}
@@ -455,7 +452,7 @@
</p>
{/if}
<P2PServerStatusCard {getLiveSyncReplicator} {core} />
<P2PServerStatusCard {p2p} {core} />
<div class="peers-section">
<div class="peers-header">
@@ -2,21 +2,21 @@ import { WorkspaceLeaf } from "@/deps.ts";
import { mount } from "svelte";
import { SvelteItemView } from "@/common/SvelteItemView.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";
export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status";
export class P2PServerStatusPaneView extends SvelteItemView {
core: LiveSyncBaseCore;
private _p2pResult: P2PPaneParams;
private readonly p2p: P2PServiceViews;
override icon = "waypoints";
override navigation = false;
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams) {
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2p: P2PServiceViews) {
super(leaf);
this.core = core;
this._p2pResult = p2pResult;
this.p2p = p2p;
}
override getIcon(): string {
@@ -35,7 +35,7 @@ export class P2PServerStatusPaneView extends SvelteItemView {
return mount(P2PServerStatusPane, {
target,
props: {
getLiveSyncReplicator: () => this._p2pResult.replicator,
p2p: this.p2p,
core: this.core,
},
});
@@ -1,17 +1,16 @@
<script lang="ts">
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";
interface Props {
peerStatus: PeerStatus;
p2p: P2PReplicatorHandle;
p2p: P2PReplicatorPaneP2P;
showPeerMenu?: (peer: PeerStatus, event: MouseEvent) => void;
}
let { peerStatus, p2p, showPeerMenu }: Props = $props();
let peer = $derived(peerStatus);
const currentReplicator = () => p2p.replicator;
function select<T extends PropertyKey, U, V = undefined>(
d: T,
@@ -72,7 +71,7 @@
let isNew = $derived.by(() => peer.accepted === AcceptedStatus.UNKNOWN);
function makeDecision(isAccepted: boolean, isTemporary: boolean) {
currentReplicator().makeDecision({
void p2p.peerAdmission.makeDecision({
peerId: peer.peerId,
name: peer.name,
decision: isAccepted,
@@ -80,7 +79,7 @@
});
}
function revokeDecision() {
currentReplicator().revokeDecision({
void p2p.peerAdmission.revokeDecision({
peerId: peer.peerId,
name: peer.name,
});
@@ -99,14 +98,14 @@
return attrs;
});
function startWatching() {
currentReplicator().watchPeer(peer.peerId);
p2p.changeRelay.watchPeer(peer.peerId);
}
function stopWatching() {
currentReplicator().unwatchPeer(peer.peerId);
p2p.changeRelay.unwatchPeer(peer.peerId);
}
function sync() {
void currentReplicator().sync(peer.peerId, false);
void p2p.targetedTransfer.synchroniseWithPeer(peer.peerId, false);
}
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: {},
});
services.replication.onBeforeReplicate.addHandler(isOnlineAndCanReplicateWithHost, 10);
services.replication.onBeforeReplicate.addHandler(canReplicateWithPBKDF2WithHost, 20);
services.replication.onPrepareCentralRemoteReplication.addHandler(canReplicateWithPBKDF2WithHost);
// <-- End of handlers that can be separated.
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this), 100);
services.replication.onReplicationFailed.addHandler(this.onReplicationFailed.bind(this));
+68 -9
View File
@@ -19,12 +19,7 @@ import { ModuleReplicator } from "./ModuleReplicator";
describe("ModuleReplicator", () => {
it("refreshes the remote Security Seed before replication", async () => {
const ensurePBKDF2Salt = vi.fn(async () => true);
let beforeReplicate: ((showMessage: boolean) => Promise<boolean>) | undefined;
const addHandler = vi.fn((handler: (showMessage: boolean) => Promise<boolean>, priority?: number) => {
if (priority === 20) {
beforeReplicate = handler;
}
});
let prepareCentralRemoteReplication: ((showMessage: boolean) => Promise<boolean>) | undefined;
const services = {
API: { isOnline: true },
replicator: {
@@ -36,7 +31,12 @@ describe("ModuleReplicator", () => {
appLifecycle: { onSettingLoaded: { addHandler: vi.fn() } },
replication: {
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() },
},
};
@@ -54,13 +54,72 @@ describe("ModuleReplicator", () => {
};
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);
});
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 () => {
eventHub.offAll();
const settings = {
+9 -6
View File
@@ -96,10 +96,10 @@ export function useP2PReplicatorUI(
};
const statusFactory = (leaf: WorkspaceLeaf) => {
return new P2PServerStatusPaneView(leaf, core, p2pParams);
return new P2PServerStatusPaneView(leaf, core, replicator);
};
const legacyStatusFactory = (leaf: WorkspaceLeaf) => {
return new LegacyP2PStatusPaneView(leaf, core, p2pParams);
return new LegacyP2PStatusPaneView(leaf, core, replicator);
};
const openStatusPane = () => {
if (api.showWindowOnRight) {
@@ -167,7 +167,7 @@ export function useP2PReplicatorUI(
const isAvailable =
hasP2PConfiguration(settings) &&
settings.remoteType !== REMOTE_P2P &&
(replicator.replicator?.server?.isServing ?? false);
replicator.transportLifecycle.isConnected;
if (!isAvailable) return false;
if (!isChecking) {
runOpenReplication();
@@ -183,7 +183,7 @@ export function useP2PReplicatorUI(
const isAvailable =
hasP2PConfiguration(settings) &&
settings.remoteType !== REMOTE_P2P &&
(replicator.replicator?.server?.isServing ?? false);
replicator.transportLifecycle.isConnected;
if (!isAvailable) return false;
if (!isChecking) {
runOpenReplication();
@@ -198,10 +198,13 @@ export function useP2PReplicatorUI(
checkCallback: (isChecking: boolean) => {
const isAvailable =
hasP2PConfiguration(host.services.setting.currentSettings()) &&
(replicator.replicator?.server?.isServing ?? false);
replicator.transportLifecycle.isConnected;
if (!isAvailable) return false;
if (!isChecking) {
void replicator.replicator?.replicateFromCommand(true);
void host.services.replicator.runFiniteReplicationActivity(
() => replicator.targetedTransfer.synchroniseConfiguredTargets(),
{ label: "replication" }
);
}
return true;
},
@@ -95,6 +95,7 @@ describe("useP2PReplicatorUI commands", () => {
},
} as any;
const p2p = {
transportLifecycle: { isConnected: true },
replicator: {
server: { isServing: true },
openReplication,
@@ -209,6 +210,7 @@ describe("useP2PReplicatorUI commands", () => {
remoteType: "COUCHDB",
remoteConfigurations: {},
};
const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task());
const host = {
services: {
context: createServiceContext(),
@@ -232,14 +234,18 @@ describe("useP2PReplicatorUI commands", () => {
currentSettings: vi.fn(() => settings),
onSettingSaved: { addHandler: vi.fn() },
},
replicator: { runFiniteReplicationActivity: vi.fn() },
replicator: { runFiniteReplicationActivity },
},
} as any;
const replicateFromCommand = vi.fn();
const synchroniseConfiguredTargets = vi.fn(async () => ({ status: "completed" }));
const p2p = {
transportLifecycle: { isConnected: true },
targetedTransfer: { synchroniseConfiguredTargets },
replicator: {
server: { isServing: true },
openReplication: vi.fn(),
replicateFromCommand: vi.fn(),
replicateFromCommand,
},
} as any;
@@ -274,6 +280,13 @@ describe("useP2PReplicatorUI commands", () => {
]) {
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 () => {