mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-20 08:12:31 +00:00
Migrate P2P UI and CLI transfers to service views
This commit is contained in:
@@ -17,6 +17,7 @@ function delay(ms: number): Promise<void> {
|
|||||||
return new Promise((resolve) => compatGlobal.setTimeout(resolve, ms));
|
return new Promise((resolve) => compatGlobal.setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse a CLI timeout expressed as a finite, non-negative number of seconds. */
|
||||||
export function parseTimeoutSeconds(value: string, commandName: string): number {
|
export function parseTimeoutSeconds(value: string, commandName: string): number {
|
||||||
const timeoutSec = Number(value);
|
const timeoutSec = Number(value);
|
||||||
if (!Number.isFinite(timeoutSec) || timeoutSec < 0) {
|
if (!Number.isFinite(timeoutSec) || timeoutSec < 0) {
|
||||||
@@ -54,6 +55,7 @@ function getSortedPeers(service: Pick<P2PServiceViews, "peerDirectory">): CLIP2P
|
|||||||
.sort((a, b) => a.peerId.localeCompare(b.peerId));
|
.sort((a, b) => a.peerId.localeCompare(b.peerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Connect for a bounded discovery interval, return a stable peer ordering, and disconnect. */
|
||||||
export async function collectPeers(
|
export async function collectPeers(
|
||||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||||
p2pService: CLIP2PService | undefined,
|
p2pService: CLIP2PService | undefined,
|
||||||
@@ -137,6 +139,7 @@ export function createPeerConnectionStatsPayload(
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve one peer token, complete pull then push, and disconnect on every settlement. */
|
||||||
export async function syncWithPeer(
|
export async function syncWithPeer(
|
||||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||||
p2pService: CLIP2PService | undefined,
|
p2pService: CLIP2PService | undefined,
|
||||||
@@ -167,6 +170,9 @@ export async function syncWithPeer(
|
|||||||
if (pullResult && "error" in pullResult && pullResult.error) {
|
if (pullResult && "error" in pullResult && pullResult.error) {
|
||||||
throw pullResult.error instanceof Error ? pullResult.error : LiveSyncError.fromError(pullResult.error);
|
throw pullResult.error instanceof Error ? pullResult.error : LiveSyncError.fromError(pullResult.error);
|
||||||
}
|
}
|
||||||
|
if (!pullResult || pullResult.status !== "completed") {
|
||||||
|
throw LiveSyncError.fromError("P2P sync failed while pulling from peer");
|
||||||
|
}
|
||||||
const pushResult = await service.targetedTransfer.requestPushToPeer(targetPeer.peerId);
|
const pushResult = await service.targetedTransfer.requestPushToPeer(targetPeer.peerId);
|
||||||
if (!pushResult || pushResult.ok !== true) {
|
if (!pushResult || pushResult.ok !== true) {
|
||||||
const err: unknown = pushResult && "error" in pushResult ? pushResult.error : undefined;
|
const err: unknown = pushResult && "error" in pushResult ? pushResult.error : undefined;
|
||||||
@@ -182,6 +188,7 @@ export async function syncWithPeer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Connect the headless P2P host and transfer transport ownership to the caller. */
|
||||||
export async function openP2PHost(
|
export async function openP2PHost(
|
||||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||||
p2pService: CLIP2PService | undefined
|
p2pService: CLIP2PService | undefined
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function createCore() {
|
|||||||
function createP2PService() {
|
function createP2PService() {
|
||||||
const connect = vi.fn(async () => undefined);
|
const connect = vi.fn(async () => undefined);
|
||||||
const disconnect = vi.fn(async () => undefined);
|
const disconnect = vi.fn(async () => undefined);
|
||||||
const pullFromPeer = vi.fn(async () => ({ ok: true }));
|
const pullFromPeer = vi.fn(async () => ({ status: "completed" as const, ok: true as const }));
|
||||||
const requestPushToPeer = vi.fn(async () => ({ ok: true }));
|
const requestPushToPeer = vi.fn(async () => ({ status: "completed" as const, ok: true as const }));
|
||||||
return {
|
return {
|
||||||
service: {
|
service: {
|
||||||
transportLifecycle: { isConnected: false, connect, disconnect },
|
transportLifecycle: { isConnected: false, connect, disconnect },
|
||||||
@@ -72,6 +72,16 @@ describe("p2p command helpers", () => {
|
|||||||
expect(requestPushToPeer).toHaveBeenCalledWith("peer-a");
|
expect(requestPushToPeer).toHaveBeenCalledWith("peer-a");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects a cancelled pull without requesting a peer push", async () => {
|
||||||
|
const { service, disconnect, pullFromPeer, requestPushToPeer } = createP2PService();
|
||||||
|
pullFromPeer.mockResolvedValue({ status: "cancelled" } as never);
|
||||||
|
|
||||||
|
await expect(syncWithPeer(createCore(), service as never, "peer-a", 0)).rejects.toBeDefined();
|
||||||
|
|
||||||
|
expect(requestPushToPeer).not.toHaveBeenCalled();
|
||||||
|
expect(disconnect).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves the benchmark diagnostics JSONL contract", () => {
|
it("preserves the benchmark diagnostics JSONL contract", () => {
|
||||||
expect(
|
expect(
|
||||||
createPeerConnectionStatsPayload(
|
createPeerConnectionStatsPayload(
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import P2POpenReplicationPane from "./P2POpenReplicationPane.svelte";
|
|||||||
import { mount, unmount } from "svelte";
|
import { mount, unmount } from "svelte";
|
||||||
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
|
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports action completion so the pane does not infer success merely from a
|
||||||
|
* settled Promise.
|
||||||
|
*/
|
||||||
export type P2POpenReplicationModalCallback = {
|
export type P2POpenReplicationModalCallback = {
|
||||||
onSync: (peerId: string) => Promise<void>;
|
onSync: (peerId: string) => Promise<boolean>;
|
||||||
onSyncAndClose: (peerId: string) => Promise<void>;
|
onSyncAndClose: (peerId: string) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class P2POpenReplicationModal extends Modal {
|
export class P2POpenReplicationModal extends Modal {
|
||||||
@@ -35,17 +39,20 @@ export class P2POpenReplicationModal extends Modal {
|
|||||||
this.rebuildMode = rebuildMode;
|
this.rebuildMode = rebuildMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onSync(peerId: string) {
|
async onSync(peerId: string): Promise<boolean> {
|
||||||
if (this.callback?.onSync) {
|
if (this.callback?.onSync) {
|
||||||
await this.callback.onSync(peerId);
|
return await this.callback.onSync(peerId);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onSyncAndClose(peerId: string) {
|
async onSyncAndClose(peerId: string): Promise<boolean> {
|
||||||
|
let completed = false;
|
||||||
if (this.callback?.onSyncAndClose) {
|
if (this.callback?.onSyncAndClose) {
|
||||||
await this.callback.onSyncAndClose(peerId);
|
completed = await this.callback.onSyncAndClose(peerId);
|
||||||
}
|
}
|
||||||
this.close();
|
this.close();
|
||||||
|
return completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onOpen() {
|
override onOpen() {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
p2p: P2PServiceViews;
|
p2p: P2PServiceViews;
|
||||||
onSync: (_peerId: string) => Promise<void>;
|
onSync: (_peerId: string) => Promise<boolean>;
|
||||||
onSyncAndClose: (_peerId: string) => Promise<void>;
|
onSyncAndClose: (_peerId: string) => Promise<boolean>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
showResult: boolean;
|
showResult: boolean;
|
||||||
rebuildMode?: boolean;
|
rebuildMode?: boolean;
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
try {
|
try {
|
||||||
syncingPeerId = peerId;
|
syncingPeerId = peerId;
|
||||||
Logger(`Starting sync with ${peerId}`, logLevel);
|
Logger(`Starting sync with ${peerId}`, logLevel);
|
||||||
await onSync(peerId);
|
const completed = await onSync(peerId);
|
||||||
Logger(`Sync completed with ${peerId}`, logLevel);
|
if (completed) Logger(`Sync completed with ${peerId}`, logLevel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel);
|
Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
try {
|
try {
|
||||||
syncingPeerId = peerId;
|
syncingPeerId = peerId;
|
||||||
Logger(`Starting sync with ${peerId}`, logLevel);
|
Logger(`Starting sync with ${peerId}`, logLevel);
|
||||||
await onSyncAndClose(peerId);
|
const completed = await onSyncAndClose(peerId);
|
||||||
Logger(`Sync completed with ${peerId}`, logLevel);
|
if (completed) Logger(`Sync completed with ${peerId}`, logLevel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel);
|
Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { P2POpenReplicationModal } from "./P2POpenReplicationModal";
|
|||||||
/**
|
/**
|
||||||
* Creates an openReplicationUI factory for Obsidian environments.
|
* Creates an openReplicationUI factory for Obsidian environments.
|
||||||
* Returns a per-replicator closure that opens the P2P Replication modal
|
* Returns a per-replicator closure that opens the P2P Replication modal
|
||||||
* and performs bidirectional sync (pull then push on success).
|
* and performs bidirectional sync (pull then push on success) through the
|
||||||
|
* stable targeted-transfer view. The compatibility Replicator argument is
|
||||||
|
* intentionally unused here and remains available only to the rebuild factory.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* const factory = createOpenReplicationUI(app);
|
* const factory = createOpenReplicationUI(app);
|
||||||
@@ -17,7 +19,7 @@ import { P2POpenReplicationModal } from "./P2POpenReplicationModal";
|
|||||||
export function createOpenReplicationUI(
|
export function createOpenReplicationUI(
|
||||||
app: App
|
app: App
|
||||||
): (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void> {
|
): (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) => (showResult: boolean) => Promise<boolean | void> {
|
||||||
return (replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) =>
|
return (_replicator: LiveSyncTrysteroReplicator, p2p: P2PServiceViews) =>
|
||||||
(showResult: boolean): Promise<boolean | void> => {
|
(showResult: boolean): Promise<boolean | void> => {
|
||||||
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||||
return new Promise<boolean | void>((resolve) => {
|
return new Promise<boolean | void>((resolve) => {
|
||||||
@@ -37,20 +39,25 @@ export function createOpenReplicationUI(
|
|||||||
activeSynchronisations++;
|
activeSynchronisations++;
|
||||||
try {
|
try {
|
||||||
// Pull first, then push only when the pull succeeds.
|
// Pull first, then push only when the pull succeeds.
|
||||||
const pullResult = await replicator.replicateFrom(peerId, showResult);
|
const pullResult = await p2p.targetedTransfer.pullFromPeer(peerId, {
|
||||||
if (!pullResult?.ok) {
|
showNotice: showResult,
|
||||||
|
});
|
||||||
|
if (pullResult.status !== "completed" || !pullResult.ok) {
|
||||||
sessionResult = false;
|
sessionResult = false;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
const pushResult = await replicator.requestSynchroniseToPeer(peerId);
|
const pushResult = await p2p.targetedTransfer.requestPushToPeer(peerId);
|
||||||
sessionResult = pushResult?.ok ?? true;
|
const completed = pushResult.status === "completed" && pushResult.ok === true;
|
||||||
if (sessionResult && closeConnection) await replicator.close();
|
sessionResult = completed;
|
||||||
|
if (completed && closeConnection) await p2p.transportLifecycle.disconnect();
|
||||||
|
return completed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger(
|
Logger(
|
||||||
`Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
|
`Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
logLevel
|
logLevel
|
||||||
);
|
);
|
||||||
sessionResult = false;
|
sessionResult = false;
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
activeSynchronisations--;
|
activeSynchronisations--;
|
||||||
settleClosedSession();
|
settleClosedSession();
|
||||||
@@ -114,12 +121,14 @@ export function createOpenRebuildUI(
|
|||||||
Logger(`Rebuilding from peer ${peerId}`, logLevel);
|
Logger(`Rebuilding from peer ${peerId}`, logLevel);
|
||||||
const result = await replicator.replicateFrom(peerId, showResult, true);
|
const result = await replicator.replicateFrom(peerId, showResult, true);
|
||||||
sessionResult = result?.ok ?? false;
|
sessionResult = result?.ok ?? false;
|
||||||
|
return sessionResult;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger(
|
Logger(
|
||||||
`Error in rebuild from ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
|
`Error in rebuild from ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
logLevel
|
logLevel
|
||||||
);
|
);
|
||||||
sessionResult = false;
|
sessionResult = false;
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
replicator.clearOnSetup();
|
replicator.clearOnSetup();
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ const modalState = vi.hoisted(() => ({
|
|||||||
instances: [] as Array<{
|
instances: [] as Array<{
|
||||||
p2p: unknown;
|
p2p: unknown;
|
||||||
callback: {
|
callback: {
|
||||||
onSync: (peerId: string) => Promise<void>;
|
onSync: (peerId: string) => Promise<boolean>;
|
||||||
onSyncAndClose: (peerId: string) => Promise<void>;
|
onSyncAndClose: (peerId: string) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
onClosed?: () => void;
|
onClosed?: () => void;
|
||||||
open: ReturnType<typeof vi.fn>;
|
open: ReturnType<typeof vi.fn>;
|
||||||
@@ -41,8 +41,8 @@ import { createOpenRebuildUI, createOpenReplicationUI } from "./P2PReplicationUI
|
|||||||
|
|
||||||
function createReplicator() {
|
function createReplicator() {
|
||||||
return {
|
return {
|
||||||
replicateFrom: vi.fn(async () => ({ ok: true })),
|
replicateFrom: vi.fn(async () => ({ status: "completed" as const, ok: true as const })),
|
||||||
requestSynchroniseToPeer: vi.fn(async () => ({ ok: true })),
|
requestSynchroniseToPeer: vi.fn(async () => ({ status: "completed" as const, ok: true as const })),
|
||||||
close: vi.fn(async () => undefined),
|
close: vi.fn(async () => undefined),
|
||||||
setOnSetup: vi.fn(),
|
setOnSetup: vi.fn(),
|
||||||
clearOnSetup: vi.fn(),
|
clearOnSetup: vi.fn(),
|
||||||
@@ -50,7 +50,16 @@ function createReplicator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createP2PServiceViews() {
|
function createP2PServiceViews() {
|
||||||
return { transportLifecycle: {}, diagnostics: {} } as any;
|
return {
|
||||||
|
transportLifecycle: {
|
||||||
|
disconnect: vi.fn(async () => undefined),
|
||||||
|
},
|
||||||
|
targetedTransfer: {
|
||||||
|
pullFromPeer: vi.fn(async () => ({ status: "completed" as const, ok: true as const })),
|
||||||
|
requestPushToPeer: vi.fn(async () => ({ status: "completed" as const, ok: true as const })),
|
||||||
|
},
|
||||||
|
diagnostics: {},
|
||||||
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("createOpenReplicationUI", () => {
|
describe("createOpenReplicationUI", () => {
|
||||||
@@ -72,35 +81,53 @@ describe("createOpenReplicationUI", () => {
|
|||||||
|
|
||||||
it("keeps repeated synchronisation inside the session boundary until the modal closes", async () => {
|
it("keeps repeated synchronisation inside the session boundary until the modal closes", async () => {
|
||||||
const replicator = createReplicator();
|
const replicator = createReplicator();
|
||||||
const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
|
const p2p = createP2PServiceViews();
|
||||||
|
const session = createOpenReplicationUI({} as any)(replicator, p2p)(true);
|
||||||
const modal = modalState.instances[0];
|
const modal = modalState.instances[0];
|
||||||
let settled = false;
|
let settled = false;
|
||||||
void session.finally(() => {
|
void session.finally(() => {
|
||||||
settled = true;
|
settled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
await modal.callback.onSync("peer-a");
|
await expect(modal.callback.onSync("peer-a")).resolves.toBe(true);
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
expect(settled).toBe(false);
|
expect(settled).toBe(false);
|
||||||
await modal.callback.onSync("peer-b");
|
await modal.callback.onSync("peer-b");
|
||||||
expect(replicator.replicateFrom).toHaveBeenCalledTimes(2);
|
expect(p2p.targetedTransfer.pullFromPeer).toHaveBeenCalledTimes(2);
|
||||||
expect(replicator.requestSynchroniseToPeer).toHaveBeenCalledTimes(2);
|
expect(p2p.targetedTransfer.requestPushToPeer).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
modal.onClosed?.();
|
modal.onClosed?.();
|
||||||
await expect(session).resolves.toBe(true);
|
await expect(session).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("waits for an in-flight synchronisation when the modal closes", async () => {
|
it("routes ordinary peer transfer through the stable targeted-transfer view", async () => {
|
||||||
let finishPull!: (value: { ok: boolean }) => void;
|
|
||||||
const replicator = createReplicator();
|
const replicator = createReplicator();
|
||||||
replicator.replicateFrom.mockImplementation(
|
const p2p = createP2PServiceViews();
|
||||||
|
const session = createOpenReplicationUI({} as any)(replicator, p2p)(true);
|
||||||
|
const modal = modalState.instances[0];
|
||||||
|
|
||||||
|
await modal.callback.onSync("peer-a");
|
||||||
|
modal.onClosed?.();
|
||||||
|
await expect(session).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(p2p.targetedTransfer.pullFromPeer).toHaveBeenCalledWith("peer-a", { showNotice: true });
|
||||||
|
expect(p2p.targetedTransfer.requestPushToPeer).toHaveBeenCalledWith("peer-a");
|
||||||
|
expect(replicator.replicateFrom).not.toHaveBeenCalled();
|
||||||
|
expect(replicator.requestSynchroniseToPeer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits for an in-flight synchronisation when the modal closes", async () => {
|
||||||
|
let finishPull!: (value: { status: "completed"; ok: true }) => void;
|
||||||
|
const replicator = createReplicator();
|
||||||
|
const p2p = createP2PServiceViews();
|
||||||
|
p2p.targetedTransfer.pullFromPeer.mockImplementation(
|
||||||
async () =>
|
async () =>
|
||||||
await new Promise<{ ok: boolean }>((resolve) => {
|
await new Promise<{ status: "completed"; ok: true }>((resolve) => {
|
||||||
finishPull = resolve;
|
finishPull = resolve;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
|
const session = createOpenReplicationUI({} as any)(replicator, p2p)(true);
|
||||||
const modal = modalState.instances[0];
|
const modal = modalState.instances[0];
|
||||||
let settled = false;
|
let settled = false;
|
||||||
void session.finally(() => {
|
void session.finally(() => {
|
||||||
@@ -113,19 +140,21 @@ describe("createOpenReplicationUI", () => {
|
|||||||
|
|
||||||
expect(settled).toBe(false);
|
expect(settled).toBe(false);
|
||||||
|
|
||||||
finishPull({ ok: true });
|
finishPull({ status: "completed", ok: true });
|
||||||
await synchronisation;
|
await synchronisation;
|
||||||
await expect(session).resolves.toBe(true);
|
await expect(session).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("closes the P2P connection after a successful sync-and-close action", async () => {
|
it("closes the P2P connection after a successful sync-and-close action", async () => {
|
||||||
const replicator = createReplicator();
|
const replicator = createReplicator();
|
||||||
const session = createOpenReplicationUI({} as any)(replicator, createP2PServiceViews())(true);
|
const p2p = createP2PServiceViews();
|
||||||
|
const session = createOpenReplicationUI({} as any)(replicator, p2p)(true);
|
||||||
const modal = modalState.instances[0];
|
const modal = modalState.instances[0];
|
||||||
|
|
||||||
await modal.callback.onSyncAndClose("peer-a");
|
await modal.callback.onSyncAndClose("peer-a");
|
||||||
|
|
||||||
expect(replicator.close).toHaveBeenCalledOnce();
|
expect(p2p.transportLifecycle.disconnect).toHaveBeenCalledOnce();
|
||||||
|
expect(replicator.close).not.toHaveBeenCalled();
|
||||||
let settled = false;
|
let settled = false;
|
||||||
void session.finally(() => {
|
void session.finally(() => {
|
||||||
settled = true;
|
settled = true;
|
||||||
@@ -136,6 +165,20 @@ describe("createOpenReplicationUI", () => {
|
|||||||
modal.onClosed?.();
|
modal.onClosed?.();
|
||||||
await expect(session).resolves.toBe(true);
|
await expect(session).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns a cancelled peer push as non-success to the presentation boundary", async () => {
|
||||||
|
const replicator = createReplicator();
|
||||||
|
const p2p = createP2PServiceViews();
|
||||||
|
p2p.targetedTransfer.requestPushToPeer.mockResolvedValue({ status: "cancelled" } as never);
|
||||||
|
const session = createOpenReplicationUI({} as any)(replicator, p2p)(true);
|
||||||
|
const modal = modalState.instances[0];
|
||||||
|
|
||||||
|
const actionResult = await modal.callback.onSync("peer-a");
|
||||||
|
modal.onClosed?.();
|
||||||
|
|
||||||
|
expect(actionResult).toBe(false);
|
||||||
|
await expect(session).resolves.toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createOpenRebuildUI", () => {
|
describe("createOpenRebuildUI", () => {
|
||||||
@@ -144,11 +187,11 @@ describe("createOpenRebuildUI", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("waits for an in-flight rebuild when the modal closes", async () => {
|
it("waits for an in-flight rebuild when the modal closes", async () => {
|
||||||
let finishPull!: (value: { ok: boolean }) => void;
|
let finishPull!: (value: { status: "completed"; ok: true }) => void;
|
||||||
const replicator = createReplicator();
|
const replicator = createReplicator();
|
||||||
replicator.replicateFrom.mockImplementation(
|
replicator.replicateFrom.mockImplementation(
|
||||||
async () =>
|
async () =>
|
||||||
await new Promise<{ ok: boolean }>((resolve) => {
|
await new Promise<{ status: "completed"; ok: true }>((resolve) => {
|
||||||
finishPull = resolve;
|
finishPull = resolve;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -165,8 +208,8 @@ describe("createOpenRebuildUI", () => {
|
|||||||
|
|
||||||
expect(settled).toBe(false);
|
expect(settled).toBe(false);
|
||||||
|
|
||||||
finishPull({ ok: true });
|
finishPull({ status: "completed", ok: true });
|
||||||
await rebuild;
|
await expect(rebuild).resolves.toBe(true);
|
||||||
await expect(session).resolves.toBe(true);
|
await expect(session).resolves.toBe(true);
|
||||||
expect(replicator.setOnSetup).toHaveBeenCalledOnce();
|
expect(replicator.setOnSetup).toHaveBeenCalledOnce();
|
||||||
expect(replicator.replicateFrom).toHaveBeenCalledWith("peer-a", true, true);
|
expect(replicator.replicateFrom).toHaveBeenCalledWith("peer-a", true, true);
|
||||||
|
|||||||
@@ -106,12 +106,12 @@
|
|||||||
throw new Error("The P2P Setup connection probe is not available.");
|
throw new Error("The P2P Setup connection probe is not available.");
|
||||||
}
|
}
|
||||||
const result = await coordinateP2PSetupConnectionProbe(admission, trialRemoteSetting, async () => {
|
const result = await coordinateP2PSetupConnectionProbe(admission, trialRemoteSetting, async () => {
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, unknown>();
|
||||||
const store = {
|
const store = {
|
||||||
get: (key: string) => {
|
get: (key: string) => {
|
||||||
return Promise.resolve(map.get(key) || null);
|
return Promise.resolve(map.get(key) || null);
|
||||||
},
|
},
|
||||||
set: (key: string, value: any) => {
|
set: (key: string, value: unknown) => {
|
||||||
map.set(key, value);
|
map.set(key, value);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
get db() {
|
get db() {
|
||||||
return Promise.resolve(this);
|
return Promise.resolve(this);
|
||||||
},
|
},
|
||||||
} as SimpleStore<any>;
|
} as SimpleStore<unknown>;
|
||||||
|
|
||||||
const dummyPouch = new PouchDB<EntryDoc>("dummy");
|
const dummyPouch = new PouchDB<EntryDoc>("dummy");
|
||||||
let replicator: TrysteroReplicator | undefined;
|
let replicator: TrysteroReplicator | undefined;
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
events: context.context.events,
|
events: context.context.events,
|
||||||
translate: context.context.translate,
|
translate: context.context.translate,
|
||||||
settings: trialRemoteSetting,
|
settings: trialRemoteSetting,
|
||||||
processReplicatedDocs: async (_docs: any[]) => {
|
processReplicatedDocs: async (_docs: PouchDB.Core.ExistingDocument<EntryDoc>[]) => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
confirm: context.services.confirm,
|
confirm: context.services.confirm,
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export function useP2PReplicatorUI(
|
|||||||
const activeReplicator = replicator.replicator;
|
const activeReplicator = replicator.replicator;
|
||||||
if (!activeReplicator) return;
|
if (!activeReplicator) return;
|
||||||
const settings = host.services.setting.currentSettings();
|
const settings = host.services.setting.currentSettings();
|
||||||
|
// The deprecated compatibility result only opens the ordinary UI; the
|
||||||
|
// actual transfer runs through the stable targeted-transfer view.
|
||||||
void host.services.replicator.runFiniteReplicationActivity(
|
void host.services.replicator.runFiniteReplicationActivity(
|
||||||
() => activeReplicator.openReplication(settings, false, true, false),
|
() => activeReplicator.openReplication(settings, false, true, false),
|
||||||
{ label: "replication" }
|
{ label: "replication" }
|
||||||
|
|||||||
Reference in New Issue
Block a user