refactor: compose browser application runtimes

This commit is contained in:
vorotamoroz
2026-07-29 09:11:57 +00:00
parent b22c1bd777
commit 4723771ee0
65 changed files with 3918 additions and 2770 deletions
@@ -1,14 +1,14 @@
<script lang="ts">
import { onMount, setContext } from "svelte";
import { onMount } from "svelte";
import { AutoAccepting, DEFAULT_SETTINGS, type P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types";
import {
AcceptedStatus,
ConnectionStatus,
type PeerStatus,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import type { P2PReplicatorPaneController } from "./P2PReplicatorPaneController";
import type { P2PReplicatorPaneHost } from "./P2PReplicatorPaneHost";
import PeerStatusRow from "@/features/P2PSync/P2PReplicator/PeerStatusRow.svelte";
import { EVENT_LAYOUT_READY, eventHub } from "@/common/events";
import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
import {
type PeerInfo,
type P2PServerInfo,
@@ -20,16 +20,16 @@
import { $msg as _msg } from "@/common/translation";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { generateP2PRoomId } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
interface Props {
getCmdSync: () => P2PReplicatorPaneController;
core: Pick<LiveSyncBaseCore, "services">;
host: P2PReplicatorPaneHost;
}
let { getCmdSync, core }: Props = $props();
setContext("getReplicator", () => getCmdSync());
const currentSettings = () => core.services.setting.currentSettings() as P2PSyncSetting;
let { host }: Props = $props();
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);
@@ -81,7 +81,7 @@
// P2P_AutoStart: eAutoStart,
// P2P_AutoBroadcast: eAutoBroadcast,
// };
await core.services.setting.applyPartial(
await services.setting.applyPartial(
{
P2P_Enabled: eP2PEnabled,
P2P_relays: eRelay,
@@ -94,7 +94,7 @@
},
true
);
core.services.config.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, eDeviceName);
services.config.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, eDeviceName);
deviceName = eDeviceName;
}
async function revert() {
@@ -113,7 +113,7 @@
const applyLoadSettings = (d: P2PSyncSetting, force: boolean) => {
if (force) {
const initDeviceName =
core.services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? core.services.vault.getVaultName();
services.config.getSmallConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? services.vault.getVaultName();
deviceName = initDeviceName;
eDeviceName = initDeviceName;
}
@@ -131,21 +131,22 @@
settings = d;
};
onMount(() => {
const r = eventHub.onEvent("setting-saved", async (d) => {
const r = events.onEvent("setting-saved", async (d) => {
applyLoadSettings(d, false);
closeServer();
});
const rx = eventHub.onEvent(EVENT_LAYOUT_READY, () => {
const rx = events.onEvent(EVENT_LAYOUT_READY, () => {
applyLoadSettings(currentSettings(), true);
});
const r2 = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
const r2 = events.onEvent(EVENT_SERVER_STATUS, (status) => {
serverInfo = status;
advertisements = status?.knownAdvertisements ?? [];
});
const r3 = eventHub.onEvent(EVENT_P2P_REPLICATOR_STATUS, (status) => {
const r3 = events.onEvent(EVENT_P2P_REPLICATOR_STATUS, (status) => {
replicatorInfo = status;
});
eventHub.emitEvent(EVENT_REQUEST_STATUS);
applyLoadSettings(currentSettings(), true);
events.emitEvent(EVENT_REQUEST_STATUS);
return () => {
r();
rx();
@@ -222,22 +223,22 @@
}
async function openServer() {
await getCmdSync().open();
await currentReplicator().open();
}
async function closeServer() {
await getCmdSync().close();
await currentReplicator().close();
}
function startBroadcasting() {
void getCmdSync().enableBroadcastChanges();
currentReplicator().enableBroadcastChanges();
}
function stopBroadcasting() {
void getCmdSync().disableBroadcastChanges();
currentReplicator().disableBroadcastChanges();
}
const initialDialogStatusKey = `p2p-dialog-status`;
const getDialogStatus = () => {
try {
const initialDialogStatus = JSON.parse(core.services.config.getSmallConfig(initialDialogStatusKey) ?? "{}") as {
const initialDialogStatus = JSON.parse(services.config.getSmallConfig(initialDialogStatusKey) ?? "{}") as {
notice?: boolean;
setting?: boolean;
};
@@ -254,10 +255,10 @@
notice: isNoticeOpened,
setting: isSettingOpened,
};
core.services.config.setSmallConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
services.config.setSmallConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
});
let isObsidian = $derived.by(() => {
return core.services.API.getPlatform() === "obsidian";
return services.API.getPlatform() === "obsidian";
});
</script>
@@ -434,7 +435,7 @@
</thead>
<tbody>
{#each peers as peer}
<PeerStatusRow peerStatus={peer}></PeerStatusRow>
<PeerStatusRow peerStatus={peer} p2p={host.p2p} showPeerMenu={host.showPeerMenu}></PeerStatusRow>
{/each}
</tbody>
</table>
@@ -1,17 +0,0 @@
import type {
AcceptanceDecision,
RevokeAcceptanceDecision,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
/** The operations used by the shared P2P pane, independent of its replicator implementation. */
export interface P2PReplicatorPaneController {
open(): Promise<void>;
close(): Promise<void>;
enableBroadcastChanges(): void;
disableBroadcastChanges(): void;
makeDecision(decision: AcceptanceDecision): Promise<void>;
revokeDecision(decision: RevokeAcceptanceDecision): Promise<void>;
watchPeer(peerId: string): void;
unwatchPeer(peerId: string): void;
sync(peerId: string, showNotice?: boolean): Promise<unknown>;
}
@@ -0,0 +1,12 @@
import type { RequiredServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
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">;
/** Host capabilities consumed by the shared P2P pane. */
export interface P2PReplicatorPaneHost {
readonly services: RequiredServices<"API" | "config" | "setting" | "vault">;
readonly p2p: P2PReplicatorHandle;
readonly showPeerMenu?: (peer: PeerStatus, event: MouseEvent) => void;
}
@@ -2,12 +2,11 @@ import { Menu, WorkspaceLeaf } from "@/deps.ts";
import ReplicatorPaneComponent from "./P2PReplicatorPane.svelte";
import { mount } from "svelte";
import { SvelteItemView } from "@/common/SvelteItemView.ts";
import { eventHub } from "@/common/events.ts";
import { unique } from "octagonal-wheels/collection";
import { LOG_LEVEL_NOTICE, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU, type PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
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";
export const VIEW_TYPE_P2P = "p2p-replicator";
@@ -127,46 +126,45 @@ And you can also drop the local database to rebuild from the remote device.`,
super(leaf);
this.core = core;
this._p2pResult = p2pResult;
eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
if (this.m) {
this.m.hide();
}
this.m = new Menu()
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => this.replicateFrom(peer)))
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => this.replicateTo(peer)))
.addSeparator()
.addItem((item) => {
item.setTitle("🔧 Get Configuration").onClick(async () => {
await this.getRemoteConfig(peer);
});
})
.addSeparator()
.addItem((item) => {
const mark = peer.syncOnConnect ? "checkmark" : null;
item.setTitle("Toggle Sync on connect")
.onClick(async () => {
await this.toggleProp(peer, "syncOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.watchOnConnect ? "checkmark" : null;
item.setTitle("Toggle Watch on connect")
.onClick(async () => {
await this.toggleProp(peer, "watchOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.syncOnReplicationCommand ? "checkmark" : null;
item.setTitle("Toggle Sync on `Replicate now` command")
.onClick(async () => {
await this.toggleProp(peer, "syncOnReplicationCommand");
})
.setIcon(mark);
}
private showPeerMenu(peer: PeerStatus, event: MouseEvent): void {
this.m?.hide();
this.m = new Menu()
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => this.replicateFrom(peer)))
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => this.replicateTo(peer)))
.addSeparator()
.addItem((item) => {
item.setTitle("🔧 Get Configuration").onClick(async () => {
await this.getRemoteConfig(peer);
});
this.m.showAtPosition({ x: event.x, y: event.y });
});
})
.addSeparator()
.addItem((item) => {
const mark = peer.syncOnConnect ? "checkmark" : null;
item.setTitle("Toggle Sync on connect")
.onClick(async () => {
await this.toggleProp(peer, "syncOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.watchOnConnect ? "checkmark" : null;
item.setTitle("Toggle Watch on connect")
.onClick(async () => {
await this.toggleProp(peer, "watchOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.syncOnReplicationCommand ? "checkmark" : null;
item.setTitle("Toggle Sync on `Replicate now` command")
.onClick(async () => {
await this.toggleProp(peer, "syncOnReplicationCommand");
})
.setIcon(mark);
});
void this.m.showAtPosition({ x: event.x, y: event.y });
}
getViewType() {
@@ -187,8 +185,11 @@ And you can also drop the local database to rebuild from the remote device.`,
return mount(ReplicatorPaneComponent, {
target: target,
props: {
getCmdSync: () => this._p2pResult.replicator,
core: this.core,
host: {
services: this.core.services,
p2p: this._p2pResult,
showPeerMenu: (peer: PeerStatus, event: MouseEvent) => this.showPeerMenu(peer, event),
},
},
});
}
@@ -1,20 +1,22 @@
<script lang="ts">
import { getContext } from "svelte";
import { AcceptedStatus, type PeerStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import type { P2PReplicatorPaneController } from "./P2PReplicatorPaneController";
import { eventHub } from "@/common/events";
import { EVENT_P2P_PEER_SHOW_EXTRA_MENU } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon";
import type { P2PReplicatorHandle } from "./P2PReplicatorPaneHost";
interface Props {
peerStatus: PeerStatus;
p2p: P2PReplicatorHandle;
showPeerMenu?: (peer: PeerStatus, event: MouseEvent) => void;
}
let { peerStatus }: Props = $props();
let { peerStatus, p2p, showPeerMenu }: Props = $props();
let peer = $derived(peerStatus);
const currentReplicator = () => p2p.replicator;
function select<T extends string | number | symbol, U>(d: T, cond: Record<T, U>): U;
function select<T extends string | number | symbol, U, V>(d: T, cond: Record<T, U>, def: V): U | V;
function select<T extends string | number | symbol, U>(d: T, cond: Record<T, U>, def?: U): U | undefined {
function select<T extends PropertyKey, U, V = undefined>(
d: T,
cond: Partial<Record<T, U>>,
def: V | undefined = undefined
): U | V | undefined {
return d in cond ? cond[d] : def;
}
@@ -36,7 +38,7 @@
[AcceptedStatus.UNKNOWN]: "NEW",
},
""
)
) ?? ""
);
const classList = {
["SENDING"]: "connected",
@@ -57,7 +59,7 @@
let isNew = $derived.by(() => peer.accepted === AcceptedStatus.UNKNOWN);
function makeDecision(isAccepted: boolean, isTemporary: boolean) {
getReplicator().makeDecision({
currentReplicator().makeDecision({
peerId: peer.peerId,
name: peer.name,
decision: isAccepted,
@@ -65,13 +67,11 @@
});
}
function revokeDecision() {
getReplicator().revokeDecision({
currentReplicator().revokeDecision({
peerId: peer.peerId,
name: peer.name,
});
}
const getReplicator = getContext<() => P2PReplicatorPaneController>("getReplicator");
const peerAttrLabels = $derived.by(() => {
const attrs = [];
if (peer.syncOnConnect) {
@@ -86,18 +86,18 @@
return attrs;
});
function startWatching() {
getReplicator().watchPeer(peer.peerId);
currentReplicator().watchPeer(peer.peerId);
}
function stopWatching() {
getReplicator().unwatchPeer(peer.peerId);
currentReplicator().unwatchPeer(peer.peerId);
}
function sync() {
void getReplicator().sync(peer.peerId, false);
void currentReplicator().sync(peer.peerId, false);
}
function moreMenu(evt: MouseEvent) {
eventHub.emitEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, { peer, event: evt });
showPeerMenu?.(peer, evt);
}
</script>
@@ -159,7 +159,9 @@
{:else}
<button class="button" onclick={startWatching} title="live"></button>
{/if}
<button class="button" onclick={moreMenu}>...</button>
{#if showPeerMenu}
<button class="button" onclick={moreMenu}>...</button>
{/if}
</div>
</div>
{/if}