## 0.24.13

### Fixed
#### General Replication
- No longer unexpected errors occur when the replication is stopped during for some reason (e.g., network disconnection).
#### Peer-to-Peer Synchronisation
- Set-up process will not receive data from unexpected sources.
- No longer resource leaks while enabling the `broadcasting changes`
- Logs are less verbose.
- Received data is now correctly dispatched to other devices.
- `Timeout` error now more informative.
- No longer timeout error occurs for reporting the progress to other devices.
- Decision dialogues for the same thing are not shown multiply at the same time anymore.
- Disconnection of the peer-to-peer synchronisation is now more robust and less error-prone.
#### Webpeer
- Now we can toggle Peers' configuration.
### Refactored
- Cross-platform compatibility layer has been improved.
- Common events are moved to the common library.
- Displaying replication status of the peer-to-peer synchronisation is separated from the main-log-logic.
- Some file names have been changed to be more consistent.
This commit is contained in:
vorotamoroz
2025-02-17 11:33:35 +00:00
parent 90c0ff22b9
commit 405624b51b
10 changed files with 205 additions and 423 deletions

View File

@@ -0,0 +1,175 @@
import type { IObsidianModule } from "../../modules/AbstractObsidianModule";
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "./P2PReplicator/P2PReplicatorPaneView.ts";
import {
AutoAccepting,
LOG_LEVEL_NOTICE,
REMOTE_P2P,
type EntryDoc,
type P2PSyncSetting,
type RemoteDBSettings,
} from "../../lib/src/common/types.ts";
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator.ts";
import { EVENT_REQUEST_OPEN_P2P, eventHub } from "../../common/events.ts";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator.ts";
import { Logger } from "octagonal-wheels/common/logger";
import type { CommandShim } from "../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
import {
P2PReplicatorMixIn,
removeP2PReplicatorInstance,
type P2PReplicatorBase,
} from "../../lib/src/replication/trystero/P2PReplicatorCore.ts";
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
import type ObsidianLiveSyncPlugin from "../../main.ts";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
class P2PReplicatorCommandBase extends LiveSyncCommands implements P2PReplicatorBase {
storeP2PStatusLine = reactiveSource("");
getSettings(): P2PSyncSetting {
return this.plugin.settings;
}
get settings() {
return this.plugin.settings;
}
getDB() {
return this.plugin.localDatabase.localDatabase;
}
get confirm(): Confirm {
return this.plugin.confirm;
}
_simpleStore!: SimpleStore<any>;
simpleStore(): SimpleStore<any> {
return this._simpleStore;
}
constructor(plugin: ObsidianLiveSyncPlugin) {
super(plugin);
}
async handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
// console.log("Processing Replicated Docs", docs);
return await this.plugin.$$parseReplicationResult(docs as PouchDB.Core.ExistingDocument<EntryDoc>[]);
}
onunload(): void {
throw new Error("Method not implemented.");
}
onload(): void | Promise<void> {
throw new Error("Method not implemented.");
}
init() {
this._simpleStore = this.plugin.$$getSimpleStore("p2p-sync");
return Promise.resolve(this);
}
}
export class P2PReplicator
extends P2PReplicatorMixIn(P2PReplicatorCommandBase)
implements IObsidianModule, CommandShim
{
storeP2PStatusLine = reactiveSource("");
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_P2P) {
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
}
return undefined!;
}
override onunload(): void {
removeP2PReplicatorInstance();
void this.close();
}
override onload(): void | Promise<void> {
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => {
void this.openPane();
});
this.p2pLogCollector.p2pReplicationLine.onChanged((line) => {
this.storeP2PStatusLine.value = line.value;
});
}
async $everyOnInitializeDatabase(): Promise<boolean> {
await this.initialiseP2PReplicator();
return Promise.resolve(true);
}
async $allSuspendExtraSync() {
this.plugin.settings.P2P_Enabled = false;
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
this.plugin.settings.P2P_AutoBroadcast = false;
this.plugin.settings.P2P_AutoStart = false;
this.plugin.settings.P2P_AutoSyncPeers = "";
this.plugin.settings.P2P_AutoWatchPeers = "";
return await Promise.resolve(true);
}
async $everyOnLoadStart() {
return await Promise.resolve();
}
async openPane() {
await this.plugin.$$showView(VIEW_TYPE_P2P);
}
async $everyOnloadStart(): Promise<boolean> {
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
this.plugin.addCommand({
id: "open-p2p-replicator",
name: "P2P Sync : Open P2P Replicator",
callback: async () => {
await this.openPane();
},
});
this.plugin.addCommand({
id: "p2p-establish-connection",
name: "P2P Sync : Connect to the Signalling Server",
checkCallback: (isChecking) => {
if (isChecking) {
return !(this._replicatorInstance?.server?.isServing ?? false);
}
void this.open();
},
});
this.plugin.addCommand({
id: "p2p-close-connection",
name: "P2P Sync : Disconnect from the Signalling Server",
checkCallback: (isChecking) => {
if (isChecking) {
return this._replicatorInstance?.server?.isServing ?? false;
}
Logger(`Closing P2P Connection`, LOG_LEVEL_NOTICE);
void this.close();
},
});
this.plugin.addCommand({
id: "replicate-now-by-p2p",
name: "Replicate now by P2P",
checkCallback: (isChecking) => {
if (isChecking) {
if (this.settings.remoteType == REMOTE_P2P) return false;
if (!this._replicatorInstance?.server?.isServing) return false;
return true;
}
void this._replicatorInstance?.replicateFromCommand(false);
},
});
this.plugin
.addRibbonIcon("waypoints", "P2P Replicator", async () => {
await this.openPane();
})
.addClass("livesync-ribbon-replicate-p2p");
return await Promise.resolve(true);
}
$everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.P2P_Enabled && this.settings.P2P_AutoStart) {
setTimeout(() => void this.open(), 100);
}
return Promise.resolve(true);
}
}

View File

@@ -1,240 +0,0 @@
import type { IObsidianModule } from "../../modules/AbstractObsidianModule";
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "./P2PReplicator/P2PReplicatorPaneView.ts";
import { TrysteroReplicator } from "../../lib/src/replication/trystero/TrysteroReplicator.ts";
import {
AutoAccepting,
DEFAULT_SETTINGS,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
REMOTE_P2P,
type EntryDoc,
type RemoteDBSettings,
} from "../../lib/src/common/types.ts";
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
import {
LiveSyncTrysteroReplicator,
setReplicatorFunc,
} from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator.ts";
import {
EVENT_DATABASE_REBUILT,
EVENT_PLUGIN_UNLOADED,
EVENT_REQUEST_OPEN_P2P,
EVENT_SETTING_SAVED,
eventHub,
} from "../../common/events.ts";
import {
EVENT_ADVERTISEMENT_RECEIVED,
EVENT_DEVICE_LEAVED,
EVENT_P2P_REQUEST_FORCE_OPEN,
EVENT_REQUEST_STATUS,
} from "../../lib/src/replication/trystero/TrysteroReplicatorP2PServer.ts";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator.ts";
import { Logger } from "octagonal-wheels/common/logger";
import { $msg } from "../../lib/src/common/i18n.ts";
import type { CommandShim } from "./P2PReplicator/P2PReplicatorPaneCommon.ts";
export class P2PReplicator extends LiveSyncCommands implements IObsidianModule, CommandShim {
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_P2P) {
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
}
return undefined!;
}
_replicatorInstance?: TrysteroReplicator;
onunload(): void {
setReplicatorFunc(() => undefined);
void this.close();
}
onload(): void | Promise<void> {
setReplicatorFunc(() => this._replicatorInstance);
eventHub.onEvent(EVENT_ADVERTISEMENT_RECEIVED, (peerId) => this._replicatorInstance?.onNewPeer(peerId));
eventHub.onEvent(EVENT_DEVICE_LEAVED, (info) => this._replicatorInstance?.onPeerLeaved(info));
eventHub.onEvent(EVENT_REQUEST_STATUS, () => {
this._replicatorInstance?.requestStatus();
});
eventHub.onEvent(EVENT_P2P_REQUEST_FORCE_OPEN, () => {
void this.open();
});
eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => {
void this.openPane();
});
eventHub.onEvent(EVENT_DATABASE_REBUILT, async () => {
await this.initialiseP2PReplicator();
});
eventHub.onEvent(EVENT_PLUGIN_UNLOADED, () => {
void this.close();
});
eventHub.onEvent(EVENT_SETTING_SAVED, async () => {
await this.initialiseP2PReplicator();
});
// throw new Error("Method not implemented.");
}
async $everyOnInitializeDatabase(): Promise<boolean> {
await this.initialiseP2PReplicator();
return Promise.resolve(true);
}
async $allSuspendExtraSync() {
this.plugin.settings.P2P_Enabled = false;
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
this.plugin.settings.P2P_AutoBroadcast = false;
this.plugin.settings.P2P_AutoStart = false;
this.plugin.settings.P2P_AutoSyncPeers = "";
this.plugin.settings.P2P_AutoWatchPeers = "";
return await Promise.resolve(true);
}
async $everyOnLoadStart() {
return await Promise.resolve();
}
async openPane() {
await this.plugin.$$showView(VIEW_TYPE_P2P);
}
async $everyOnloadStart(): Promise<boolean> {
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
this.plugin.addCommand({
id: "open-p2p-replicator",
name: "P2P Sync : Open P2P Replicator",
callback: async () => {
await this.openPane();
},
});
this.plugin.addCommand({
id: "p2p-establish-connection",
name: "P2P Sync : Connect to the Signalling Server",
checkCallback: (isChecking) => {
if (isChecking) {
return !(this._replicatorInstance?.server?.isServing ?? false);
}
void this.open();
},
});
this.plugin.addCommand({
id: "p2p-close-connection",
name: "P2P Sync : Disconnect from the Signalling Server",
checkCallback: (isChecking) => {
if (isChecking) {
return this._replicatorInstance?.server?.isServing ?? false;
}
Logger(`Closing P2P Connection`, LOG_LEVEL_NOTICE);
void this.close();
},
});
this.plugin.addCommand({
id: "replicate-now-by-p2p",
name: "Replicate now by P2P",
checkCallback: (isChecking) => {
if (isChecking) {
if (this.settings.remoteType == REMOTE_P2P) return false;
if (!this._replicatorInstance?.server?.isServing) return false;
return true;
}
void this._replicatorInstance?.replicateFromCommand(false);
},
});
this.plugin
.addRibbonIcon("waypoints", "P2P Replicator", async () => {
await this.openPane();
})
.addClass("livesync-ribbon-replicate-p2p");
return await Promise.resolve(true);
}
$everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.P2P_Enabled && this.settings.P2P_AutoStart) {
setTimeout(() => void this.open(), 100);
}
return Promise.resolve(true);
}
async open() {
if (!this.settings.P2P_Enabled) {
this._notice($msg("P2P.NotEnabled"));
return;
}
if (!this._replicatorInstance) {
await this.initialiseP2PReplicator();
if (!this.settings.P2P_AutoStart) {
// While auto start is enabled, we don't need to open the connection (Literally, it's already opened automatically)
await this._replicatorInstance!.open();
}
} else {
await this._replicatorInstance?.open();
}
}
async close() {
await this._replicatorInstance?.close();
this._replicatorInstance = undefined;
}
getConfig(key: string) {
const vaultName = this.plugin.$$getVaultName();
const dbKey = `${vaultName}-${key}`;
return localStorage.getItem(dbKey);
}
setConfig(key: string, value: string) {
const vaultName = this.plugin.$$getVaultName();
const dbKey = `${vaultName}-${key}`;
localStorage.setItem(dbKey, value);
}
async initialiseP2PReplicator(): Promise<TrysteroReplicator> {
const getPlugin = () => this.plugin;
try {
// const plugin = this.plugin;
if (this._replicatorInstance) {
await this._replicatorInstance.close();
this._replicatorInstance = undefined;
}
if (!this.settings.P2P_AppID) {
this.settings.P2P_AppID = DEFAULT_SETTINGS.P2P_AppID;
}
const initialDeviceName = this.getConfig("p2p_device_name") || this.plugin.$$getDeviceAndVaultName();
const env = {
get db() {
return getPlugin().localDatabase.localDatabase;
},
get confirm() {
return getPlugin().confirm;
},
get deviceName() {
return initialDeviceName;
},
platform: "wip",
get settings() {
return getPlugin().settings;
},
async processReplicatedDocs(docs: EntryDoc[]): Promise<void> {
return await getPlugin().$$parseReplicationResult(
docs as PouchDB.Core.ExistingDocument<EntryDoc>[]
);
},
simpleStore: getPlugin().$$getSimpleStore("p2p-sync"),
};
this._replicatorInstance = new TrysteroReplicator(env);
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
await this.open();
}
return this._replicatorInstance;
} catch (e) {
this._log(
e instanceof Error ? e.message : "Something occurred on Initialising P2P Replicator",
LOG_LEVEL_INFO
);
this._log(e, LOG_LEVEL_VERBOSE);
throw e;
}
}
enableBroadcastCastings() {
return this?._replicatorInstance?.enableBroadcastChanges();
}
disableBroadcastCastings() {
return this?._replicatorInstance?.disableBroadcastChanges();
}
}

View File

@@ -7,7 +7,7 @@
type CommandShim,
type PeerStatus,
type PluginShim,
} from "./P2PReplicatorPaneCommon";
} from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
import PeerStatusRow from "../P2PReplicator/PeerStatusRow.svelte";
import { EVENT_LAYOUT_READY, eventHub } from "../../../common/events";
import {
@@ -294,7 +294,12 @@
<th> Room ID </th>
<td>
<label class={{ "is-dirty": isRoomIdModified }}>
<input type="text" placeholder="anything-you-like" bind:value={eRoomId} autocomplete="off"/>
<input
type="text"
placeholder="anything-you-like"
bind:value={eRoomId}
autocomplete="off"
/>
<button onclick={() => chooseRandom()}> Use Random Number </button>
</label>
<span>
@@ -320,8 +325,7 @@
<th> This device name </th>
<td>
<label class={{ "is-dirty": isDeviceNameModified }}>
<input type="text" placeholder="iphone-16" bind:value={eDeviceName}
autocomplete="off" />
<input type="text" placeholder="iphone-16" bind:value={eDeviceName} autocomplete="off" />
</label>
</td>
</tr>

View File

@@ -1,58 +0,0 @@
import type { P2PSyncSetting } from "../../../lib/src/common/types";
export const EVENT_P2P_PEER_SHOW_EXTRA_MENU = "p2p-peer-show-extra-menu";
export enum AcceptedStatus {
UNKNOWN = "Unknown",
ACCEPTED = "Accepted",
DENIED = "Denied",
ACCEPTED_IN_SESSION = "Accepted in session",
DENIED_IN_SESSION = "Denied in session",
}
export type PeerExtraMenuEvent = {
peer: PeerStatus;
event: MouseEvent;
};
export enum ConnectionStatus {
CONNECTED = "Connected",
CONNECTED_LIVE = "Connected(live)",
DISCONNECTED = "Disconnected",
}
export type PeerStatus = {
name: string;
peerId: string;
syncOnConnect: boolean;
watchOnConnect: boolean;
syncOnReplicationCommand: boolean;
accepted: AcceptedStatus;
status: ConnectionStatus;
isFetching: boolean;
isSending: boolean;
isWatching: boolean;
};
declare global {
interface LSEvents {
[EVENT_P2P_PEER_SHOW_EXTRA_MENU]: PeerExtraMenuEvent;
// [EVENT_P2P_REPLICATOR_PROGRESS]: P2PReplicationReport;
}
}
export interface PluginShim {
saveSettings: () => Promise<void>;
settings: P2PSyncSetting;
rebuilder: any;
$$scheduleAppReload: () => void;
$$getVaultName: () => string;
// confirm: any;
}
export interface CommandShim {
getConfig(key: string): string | null;
setConfig(key: string, value: string): void;
open(): Promise<void>;
close(): Promise<void>;
enableBroadcastCastings(): void; // cmdSync._replicatorInstance?.enableBroadcastChanges();
disableBroadcastCastings(): void; ///cmdSync._replicatorInstance?.disableBroadcastChanges();
}

View File

@@ -4,11 +4,15 @@ import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { mount } from "svelte";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
import { eventHub } from "../../../common/events.ts";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU, type PeerStatus } from "./P2PReplicatorPaneCommon.ts";
import { unique } from "octagonal-wheels/collection";
import { LOG_LEVEL_NOTICE, REMOTE_P2P } from "../../../lib/src/common/types.ts";
import { Logger } from "../../../lib/src/common/logger.ts";
import { P2PReplicator } from "../CmdP2PSync.ts";
import { P2PReplicator } from "../CmdP2PReplicator.ts";
import {
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
type PeerStatus,
} from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
export const VIEW_TYPE_P2P = "p2p-replicator";
function addToList(item: string, list: string) {

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { getContext } from "svelte";
import { AcceptedStatus, type PeerStatus } from "./P2PReplicatorPaneCommon";
import type { P2PReplicator } from "../CmdP2PSync";
import { AcceptedStatus, type PeerStatus } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
import type { P2PReplicator } from "../CmdP2PReplicator";
import { eventHub } from "../../../common/events";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "./P2PReplicatorPaneCommon";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon";
interface Props {
peerStatus: PeerStatus;