mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-22 02:27:07 +00:00
Use stable P2P service views in the CLI
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { P2P_DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
||||
import { getPeerConnectionStats } from "@vrtmrz/livesync-commonlib/compat/rpc/transports/DiagRTCPeerConnections.utils";
|
||||
import type { P2PPeerConnectionMetrics, P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
|
||||
import { fsPromises } from "@vrtmrz/livesync-commonlib/node";
|
||||
|
||||
type CLIP2PPeer = {
|
||||
@@ -12,12 +11,7 @@ type CLIP2PPeer = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type CandidateSummary = {
|
||||
id: string;
|
||||
candidateType: string;
|
||||
protocol: string;
|
||||
relayProtocol: string;
|
||||
};
|
||||
type CLIP2PService = Pick<P2PServiceViews, "transportLifecycle" | "peerDirectory" | "targetedTransfer" | "diagnostics">;
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => compatGlobal.setTimeout(resolve, ms));
|
||||
@@ -43,35 +37,35 @@ function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, never>) {
|
||||
settings.P2P_IsHeadless = true;
|
||||
}
|
||||
|
||||
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
|
||||
function requireP2PService(
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
service: CLIP2PService | undefined
|
||||
): CLIP2PService {
|
||||
validateP2PSettings(core);
|
||||
const replicator = await core.services.replicator.getNewReplicator();
|
||||
if (!replicator) {
|
||||
throw new Error("Failed to create replicator instance. Ensure P2P is enabled in settings.");
|
||||
if (!service) {
|
||||
throw new Error("P2P service is not available. Ensure the P2P feature was composed for this CLI process.");
|
||||
}
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Unexpected replicator type. Expected LiveSyncTrysteroReplicator.");
|
||||
}
|
||||
return replicator;
|
||||
return service;
|
||||
}
|
||||
|
||||
function getSortedPeers(replicator: LiveSyncTrysteroReplicator): CLIP2PPeer[] {
|
||||
return [...replicator.knownAdvertisements]
|
||||
function getSortedPeers(service: Pick<P2PServiceViews, "peerDirectory">): CLIP2PPeer[] {
|
||||
return [...service.peerDirectory.getPeers()]
|
||||
.map((peer) => ({ peerId: peer.peerId, name: peer.name }))
|
||||
.sort((a, b) => a.peerId.localeCompare(b.peerId));
|
||||
}
|
||||
|
||||
export async function collectPeers(
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
p2pService: CLIP2PService | undefined,
|
||||
timeoutSec: number
|
||||
): Promise<CLIP2PPeer[]> {
|
||||
const replicator = await createReplicator(core);
|
||||
await replicator.open();
|
||||
const service = requireP2PService(core, p2pService);
|
||||
await service.transportLifecycle.connect();
|
||||
try {
|
||||
await delay(timeoutSec * 1000);
|
||||
return getSortedPeers(replicator);
|
||||
return getSortedPeers(service);
|
||||
} finally {
|
||||
await replicator.close();
|
||||
await service.transportLifecycle.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,32 +84,8 @@ function resolvePeer(peers: CLIP2PPeer[], peerToken: string): CLIP2PPeer | undef
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getReportValue<T extends string | number>(
|
||||
report: Record<string, unknown> | undefined,
|
||||
key: string
|
||||
): T | "unknown" {
|
||||
const value = report?.[key];
|
||||
return typeof value === "string" || typeof value === "number" ? (value as T) : "unknown";
|
||||
}
|
||||
|
||||
function summariseCandidate(reports: unknown[], candidateId: string): CandidateSummary | undefined {
|
||||
if (candidateId === "unknown") {
|
||||
return undefined;
|
||||
}
|
||||
const report = reports.map((r) => r as Record<string, unknown>).find((r) => r.id === candidateId);
|
||||
if (!report) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: candidateId,
|
||||
candidateType: getReportValue<string>(report, "candidateType"),
|
||||
protocol: getReportValue<string>(report, "protocol"),
|
||||
relayProtocol: getReportValue<string>(report, "relayProtocol"),
|
||||
};
|
||||
}
|
||||
|
||||
async function writePeerConnectionStatsIfRequested(
|
||||
replicator: LiveSyncTrysteroReplicator,
|
||||
service: Pick<P2PServiceViews, "diagnostics">,
|
||||
peer: CLIP2PPeer
|
||||
): Promise<void> {
|
||||
const outputPath = process.env.LIVESYNC_P2P_STATS_JSONL?.trim();
|
||||
@@ -123,21 +93,30 @@ async function writePeerConnectionStatsIfRequested(
|
||||
return;
|
||||
}
|
||||
|
||||
const peerConnection = replicator.rawHost?.room?.getPeers()[peer.peerId];
|
||||
const stats = peerConnection ? await getPeerConnectionStats(`cli-p2p-${peer.peerId}`, peerConnection) : undefined;
|
||||
const localCandidate = summariseCandidate(stats?.reports ?? [], stats?.localCandidateId ?? "unknown");
|
||||
const remoteCandidate = summariseCandidate(stats?.reports ?? [], stats?.remoteCandidateId ?? "unknown");
|
||||
const stats = await service.diagnostics.getPeerConnectionMetrics(peer.peerId);
|
||||
const payload = createPeerConnectionStatsPayload(peer, stats, new Date().toISOString());
|
||||
await fsPromises.appendFile(outputPath, `${JSON.stringify(payload)}\n`, "utf8");
|
||||
}
|
||||
|
||||
/** Build the stable JSONL record consumed by the P2P benchmark harnesses. */
|
||||
export function createPeerConnectionStatsPayload(
|
||||
peer: CLIP2PPeer,
|
||||
stats: P2PPeerConnectionMetrics | undefined,
|
||||
generatedAt: string
|
||||
) {
|
||||
const localCandidate = stats?.localCandidate;
|
||||
const remoteCandidate = stats?.remoteCandidate;
|
||||
const selectedPath =
|
||||
localCandidate && remoteCandidate
|
||||
? `${localCandidate.candidateType}<->${remoteCandidate.candidateType}`
|
||||
: "unknown";
|
||||
|
||||
const payload = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
generatedAt,
|
||||
command: "p2p-sync",
|
||||
peerId: peer.peerId,
|
||||
peerName: peer.name,
|
||||
candidatePathCollected: !!stats?.selectedPair,
|
||||
candidatePathCollected: stats?.selectedPairPresent ?? false,
|
||||
selectedPath,
|
||||
selectedPair: stats
|
||||
? {
|
||||
@@ -155,23 +134,24 @@ async function writePeerConnectionStatsIfRequested(
|
||||
localCandidate,
|
||||
remoteCandidate,
|
||||
};
|
||||
await fsPromises.appendFile(outputPath, `${JSON.stringify(payload)}\n`, "utf8");
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function syncWithPeer(
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
p2pService: CLIP2PService | undefined,
|
||||
peerToken: string,
|
||||
timeoutSec: number
|
||||
): Promise<CLIP2PPeer> {
|
||||
const replicator = await createReplicator(core);
|
||||
await replicator.open();
|
||||
const service = requireP2PService(core, p2pService);
|
||||
await service.transportLifecycle.connect();
|
||||
try {
|
||||
const timeoutMs = timeoutSec * 1000;
|
||||
const start = Date.now();
|
||||
let targetPeer: CLIP2PPeer | undefined;
|
||||
|
||||
while (Date.now() - start <= timeoutMs) {
|
||||
const peers = getSortedPeers(replicator);
|
||||
const peers = getSortedPeers(service);
|
||||
targetPeer = resolvePeer(peers, peerToken);
|
||||
if (targetPeer) {
|
||||
break;
|
||||
@@ -183,11 +163,11 @@ export async function syncWithPeer(
|
||||
throw new Error(`Peer '${peerToken}' was not found within ${timeoutSec} seconds`);
|
||||
}
|
||||
|
||||
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
|
||||
const pullResult = await service.targetedTransfer.pullFromPeer(targetPeer.peerId, { showNotice: false });
|
||||
if (pullResult && "error" in pullResult && pullResult.error) {
|
||||
throw pullResult.error instanceof Error ? pullResult.error : LiveSyncError.fromError(pullResult.error);
|
||||
}
|
||||
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
|
||||
const pushResult = await service.targetedTransfer.requestPushToPeer(targetPeer.peerId);
|
||||
if (!pushResult || pushResult.ok !== true) {
|
||||
const err: unknown = pushResult && "error" in pushResult ? pushResult.error : undefined;
|
||||
throw err instanceof Error
|
||||
@@ -195,15 +175,18 @@ export async function syncWithPeer(
|
||||
: LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync");
|
||||
}
|
||||
|
||||
await writePeerConnectionStatsIfRequested(replicator, targetPeer);
|
||||
await writePeerConnectionStatsIfRequested(service, targetPeer);
|
||||
return targetPeer;
|
||||
} finally {
|
||||
await replicator.close();
|
||||
await service.transportLifecycle.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, never>): Promise<LiveSyncTrysteroReplicator> {
|
||||
const replicator = await createReplicator(core);
|
||||
await replicator.open();
|
||||
return replicator;
|
||||
export async function openP2PHost(
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
p2pService: CLIP2PService | undefined
|
||||
): Promise<CLIP2PService> {
|
||||
const service = requireP2PService(core, p2pService);
|
||||
await service.transportLifecycle.connect();
|
||||
return service;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user