mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-18 05:21:22 +00:00
Fixed:
- No longer unexpected `Unhandled Rejections` during P2P operations (waiting acceptance). CLI new features - P2P sync has been implemented.
This commit is contained in:
149
src/apps/cli/commands/p2p.ts
Normal file
149
src/apps/cli/commands/p2p.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
|
||||
import { P2P_DEFAULT_SETTINGS, SETTING_KEY_P2P_DEVICE_NAME, type EntryDoc } from "@lib/common/types";
|
||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||
import { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator";
|
||||
|
||||
type CLIP2PPeer = {
|
||||
peerId: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function parseTimeoutSeconds(value: string, commandName: string): number {
|
||||
const timeoutSec = Number(value);
|
||||
if (!Number.isFinite(timeoutSec) || timeoutSec < 0) {
|
||||
throw new Error(`${commandName} requires a non-negative timeout in seconds`);
|
||||
}
|
||||
return timeoutSec;
|
||||
}
|
||||
|
||||
function validateP2PSettings(core: LiveSyncBaseCore<ServiceContext, any>) {
|
||||
const settings = core.services.setting.currentSettings();
|
||||
if (!settings.P2P_Enabled) {
|
||||
throw new Error("P2P is disabled in settings (P2P_Enabled=false)");
|
||||
}
|
||||
if (!settings.P2P_AppID) {
|
||||
settings.P2P_AppID = P2P_DEFAULT_SETTINGS.P2P_AppID;
|
||||
}
|
||||
// CLI mode is non-interactive.
|
||||
settings.P2P_IsHeadless = true;
|
||||
}
|
||||
|
||||
async function createReplicator(core: LiveSyncBaseCore<ServiceContext, any>): Promise<TrysteroReplicator> {
|
||||
validateP2PSettings(core);
|
||||
const getSettings = () => core.services.setting.currentSettings();
|
||||
const getDB = () => core.services.database.localDatabase.localDatabase;
|
||||
const getSimpleStore = () => core.services.keyValueDB.openSimpleStore("p2p-sync");
|
||||
const getDeviceName = () =>
|
||||
core.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) || core.services.vault.getVaultName();
|
||||
|
||||
const env = {
|
||||
get settings() {
|
||||
return getSettings();
|
||||
},
|
||||
get db() {
|
||||
return getDB();
|
||||
},
|
||||
get simpleStore() {
|
||||
return getSimpleStore();
|
||||
},
|
||||
get deviceName() {
|
||||
return getDeviceName();
|
||||
},
|
||||
get platform() {
|
||||
return core.services.API.getPlatform();
|
||||
},
|
||||
get confirm() {
|
||||
return core.services.API.confirm;
|
||||
},
|
||||
processReplicatedDocs: async (docs: EntryDoc[]) => {
|
||||
await core.services.replication.parseSynchroniseResult(docs as any);
|
||||
},
|
||||
};
|
||||
|
||||
return new TrysteroReplicator(env as any);
|
||||
}
|
||||
|
||||
function getSortedPeers(replicator: TrysteroReplicator): CLIP2PPeer[] {
|
||||
return [...replicator.knownAdvertisements]
|
||||
.map((peer) => ({ peerId: peer.peerId, name: peer.name }))
|
||||
.sort((a, b) => a.peerId.localeCompare(b.peerId));
|
||||
}
|
||||
|
||||
export async function collectPeers(
|
||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||
timeoutSec: number
|
||||
): Promise<CLIP2PPeer[]> {
|
||||
const replicator = await createReplicator(core);
|
||||
await replicator.open();
|
||||
try {
|
||||
await delay(timeoutSec * 1000);
|
||||
return getSortedPeers(replicator);
|
||||
} finally {
|
||||
await replicator.close();
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePeer(peers: CLIP2PPeer[], peerToken: string): CLIP2PPeer | undefined {
|
||||
const byId = peers.find((peer) => peer.peerId === peerToken);
|
||||
if (byId) {
|
||||
return byId;
|
||||
}
|
||||
const byName = peers.filter((peer) => peer.name === peerToken);
|
||||
if (byName.length > 1) {
|
||||
throw new Error(`Multiple peers matched by name '${peerToken}'. Use peer-id instead.`);
|
||||
}
|
||||
if (byName.length === 1) {
|
||||
return byName[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function syncWithPeer(
|
||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||
peerToken: string,
|
||||
timeoutSec: number
|
||||
): Promise<CLIP2PPeer> {
|
||||
const replicator = await createReplicator(core);
|
||||
await replicator.open();
|
||||
try {
|
||||
const timeoutMs = timeoutSec * 1000;
|
||||
const start = Date.now();
|
||||
let targetPeer: CLIP2PPeer | undefined;
|
||||
|
||||
while (Date.now() - start <= timeoutMs) {
|
||||
const peers = getSortedPeers(replicator);
|
||||
targetPeer = resolvePeer(peers, peerToken);
|
||||
if (targetPeer) {
|
||||
break;
|
||||
}
|
||||
await delay(200);
|
||||
}
|
||||
|
||||
if (!targetPeer) {
|
||||
throw new Error(`Peer '${peerToken}' was not found within ${timeoutSec} seconds`);
|
||||
}
|
||||
|
||||
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
|
||||
if (pullResult && "error" in pullResult && pullResult.error) {
|
||||
throw pullResult.error;
|
||||
}
|
||||
const pushResult = (await replicator.requestSynchroniseToPeer(targetPeer.peerId)) as any;
|
||||
if (!pushResult || pushResult.ok !== true) {
|
||||
throw pushResult?.error ?? new Error("P2P sync failed while requesting remote sync");
|
||||
}
|
||||
|
||||
return targetPeer;
|
||||
} finally {
|
||||
await replicator.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function openP2PHost(core: LiveSyncBaseCore<ServiceContext, any>): Promise<TrysteroReplicator> {
|
||||
const replicator = await createReplicator(core);
|
||||
await replicator.open();
|
||||
return replicator;
|
||||
}
|
||||
Reference in New Issue
Block a user