mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-28 22:37:08 +00:00
Adopt focused P2P service views
This commit is contained in:
@@ -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)),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,7 @@ describe("WebPeer runtime composition", () => {
|
||||
expect(runtime.context).toBe(context);
|
||||
expect(runtime.services.context).toBe(context);
|
||||
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.p2p).toBe(runtime.p2p);
|
||||
expect(runtime.paneHost.showPeerMenu).toBeTypeOf("function");
|
||||
@@ -64,6 +64,34 @@ describe("WebPeer runtime composition", () => {
|
||||
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 () => {
|
||||
const runtime = new WebPeerRuntime({
|
||||
store: createMemoryStore(),
|
||||
|
||||
Reference in New Issue
Block a user