Protect bounded remote activity across app lifecycle

This commit is contained in:
vorotamoroz
2026-07-15 07:53:16 +00:00
parent 4cbccf85b4
commit 31e9186930
373 changed files with 1275 additions and 466 deletions
@@ -69,18 +69,6 @@
}
}
async function handleSyncAndClose(peerId: string) {
fireAndForget(async () => {
try {
Logger(`Starting sync with ${peerId}`, logLevel);
await onSync(peerId);
Logger(`Sync completed with ${peerId}`, logLevel);
} catch (e) {
Logger(`Error during sync: ${e instanceof Error ? e.message : String(e)}`, logLevel);
}
});
onClose();
}
async function disconnect() {
try {
await liveSyncReplicator.close();
@@ -142,7 +130,7 @@
<button
class="btn {rebuildMode ? 'btn-primary' : 'btn-secondary'}"
disabled={syncingPeerId !== null}
onclick={() => handleSyncAndClose(peer.peerId)}
onclick={() => handleSyncThenClose(peer.peerId)}
>
{syncingPeerId === peer.peerId ? "Syncing..." : "Start Sync & Close"}
</button>
@@ -1,4 +1,4 @@
import { App } from "@/deps.ts";
import type { App } from "@/deps.ts";
import { Logger } from "@lib/common/logger";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types";
import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
@@ -20,52 +20,54 @@ export function createOpenReplicationUI(
(showResult: boolean): Promise<boolean | void> => {
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
return new Promise<boolean | void>((resolve) => {
let resolved = false;
let sessionResult = false;
let activeSynchronisations = 0;
let closed = false;
const safeResolve = () => {
if (resolved) return;
resolved = true;
resolve(sessionResult);
};
const settleClosedSession = () => {
if (closed && activeSynchronisations === 0) safeResolve();
};
const synchronise = async (peerId: string, closeConnection: boolean) => {
activeSynchronisations++;
try {
// Pull first, then push only when the pull succeeds.
const pullResult = await replicator.replicateFrom(peerId, showResult);
if (!pullResult?.ok) {
sessionResult = false;
return;
}
const pushResult = await replicator.requestSynchroniseToPeer(peerId);
sessionResult = pushResult?.ok ?? true;
if (sessionResult && closeConnection) await replicator.close();
} catch (e) {
Logger(
`Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
logLevel
);
sessionResult = false;
} finally {
activeSynchronisations--;
settleClosedSession();
}
};
const modal = new P2POpenReplicationModal(
app,
replicator,
{
onSync: async (peerId: string) => {
try {
// pull (replicateFrom) first; push only on success
const pullResult = await replicator.replicateFrom(peerId, showResult);
if (pullResult?.ok) {
const pushResult = await replicator.requestSynchroniseToPeer(peerId);
resolve(pushResult?.ok ?? true);
} else {
resolve(false);
}
} catch (e) {
Logger(
`Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
logLevel
);
resolve(false);
}
},
onSyncAndClose: async (peerId: string) => {
try {
const pullResult = await replicator.replicateFrom(peerId, showResult);
if (pullResult?.ok) {
const pushResult = await replicator.requestSynchroniseToPeer(peerId);
if (pushResult?.ok ?? true) {
await replicator.close();
resolve(true);
} else {
resolve(false);
}
} else {
resolve(false);
}
} catch (e) {
Logger(
`Error in bidirectional sync with ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
logLevel
);
resolve(false);
}
},
onSync: (peerId: string) => synchronise(peerId, false),
onSyncAndClose: (peerId: string) => synchronise(peerId, true),
},
showResult
showResult,
"P2P Replication",
() => {
closed = true;
settleClosedSession();
}
);
modal.open();
});
@@ -89,27 +91,42 @@ export function createOpenRebuildUI(
const logLevel = showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
return new Promise<boolean | void>((resolve) => {
let resolved = false;
let activeSynchronisations = 0;
let closed = false;
let operationCompleted = false;
let sessionResult = false;
const safeResolve = (val: boolean) => {
if (!resolved) {
resolved = true;
resolve(val);
}
};
const settleSession = () => {
if (activeSynchronisations !== 0) return;
if (closed || operationCompleted) safeResolve(sessionResult);
};
const doRebuild = async (peerId: string) => {
replicator.setOnSetup();
activeSynchronisations++;
try {
replicator.setOnSetup();
Logger(`Rebuilding from peer ${peerId}`, logLevel);
const result = await replicator.replicateFrom(peerId, showResult);
safeResolve(result?.ok ?? false);
sessionResult = result?.ok ?? false;
} catch (e) {
Logger(
`Error in rebuild from ${peerId}: ${e instanceof Error ? e.message : String(e)}`,
logLevel
);
safeResolve(false);
sessionResult = false;
} finally {
replicator.clearOnSetup();
try {
replicator.clearOnSetup();
} finally {
operationCompleted = true;
activeSynchronisations--;
settleSession();
}
}
};
@@ -122,7 +139,10 @@ export function createOpenRebuildUI(
},
showResult,
"P2P Rebuild",
() => safeResolve(false),
() => {
closed = true;
settleSession();
},
true
);
modal.open();
@@ -0,0 +1,165 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const modalState = vi.hoisted(() => ({
instances: [] as Array<{
callback: {
onSync: (peerId: string) => Promise<void>;
onSyncAndClose: (peerId: string) => Promise<void>;
};
onClosed?: () => void;
open: ReturnType<typeof vi.fn>;
}>,
}));
vi.mock("@/deps.ts", () => ({ App: class {} }));
vi.mock("./P2POpenReplicationModal", () => ({
P2POpenReplicationModal: class {
callback;
onClosed;
open = vi.fn();
constructor(
_app: unknown,
_replicator: unknown,
callback: (typeof modalState.instances)[number]["callback"],
_showResult: boolean,
_title?: string,
onClosed?: () => void
) {
this.callback = callback;
this.onClosed = onClosed;
modalState.instances.push(this);
}
},
}));
import { createOpenRebuildUI, createOpenReplicationUI } from "./P2PReplicationUI";
function createReplicator() {
return {
replicateFrom: vi.fn(async () => ({ ok: true })),
requestSynchroniseToPeer: vi.fn(async () => ({ ok: true })),
close: vi.fn(async () => undefined),
setOnSetup: vi.fn(),
clearOnSetup: vi.fn(),
} as any;
}
describe("createOpenReplicationUI", () => {
beforeEach(() => {
modalState.instances.length = 0;
});
it("settles a cancelled peer-selection session when the modal closes", async () => {
const session = createOpenReplicationUI({} as any)(createReplicator())(true);
const modal = modalState.instances[0];
expect(modal.onClosed).toBeTypeOf("function");
modal.onClosed?.();
await expect(session).resolves.toBe(false);
});
it("keeps repeated synchronisation inside the session boundary until the modal closes", async () => {
const replicator = createReplicator();
const session = createOpenReplicationUI({} as any)(replicator)(true);
const modal = modalState.instances[0];
let settled = false;
void session.finally(() => {
settled = true;
});
await modal.callback.onSync("peer-a");
await Promise.resolve();
expect(settled).toBe(false);
await modal.callback.onSync("peer-b");
expect(replicator.replicateFrom).toHaveBeenCalledTimes(2);
expect(replicator.requestSynchroniseToPeer).toHaveBeenCalledTimes(2);
modal.onClosed?.();
await expect(session).resolves.toBe(true);
});
it("waits for an in-flight synchronisation when the modal closes", async () => {
let finishPull!: (value: { ok: boolean }) => void;
const replicator = createReplicator();
replicator.replicateFrom.mockImplementation(
async () =>
await new Promise<{ ok: boolean }>((resolve) => {
finishPull = resolve;
})
);
const session = createOpenReplicationUI({} as any)(replicator)(true);
const modal = modalState.instances[0];
let settled = false;
void session.finally(() => {
settled = true;
});
const synchronisation = modal.callback.onSync("peer-a");
modal.onClosed?.();
await Promise.resolve();
expect(settled).toBe(false);
finishPull({ ok: true });
await synchronisation;
await expect(session).resolves.toBe(true);
});
it("closes the P2P connection after a successful sync-and-close action", async () => {
const replicator = createReplicator();
const session = createOpenReplicationUI({} as any)(replicator)(true);
const modal = modalState.instances[0];
await modal.callback.onSyncAndClose("peer-a");
expect(replicator.close).toHaveBeenCalledOnce();
let settled = false;
void session.finally(() => {
settled = true;
});
await Promise.resolve();
expect(settled).toBe(false);
modal.onClosed?.();
await expect(session).resolves.toBe(true);
});
});
describe("createOpenRebuildUI", () => {
beforeEach(() => {
modalState.instances.length = 0;
});
it("waits for an in-flight rebuild when the modal closes", async () => {
let finishPull!: (value: { ok: boolean }) => void;
const replicator = createReplicator();
replicator.replicateFrom.mockImplementation(
async () =>
await new Promise<{ ok: boolean }>((resolve) => {
finishPull = resolve;
})
);
const session = createOpenRebuildUI({} as any)(replicator)(true);
const modal = modalState.instances[0];
let settled = false;
void session.finally(() => {
settled = true;
});
const rebuild = modal.callback.onSyncAndClose("peer-a");
modal.onClosed?.();
await Promise.resolve();
expect(settled).toBe(false);
finishPull({ ok: true });
await rebuild;
await expect(session).resolves.toBe(true);
expect(replicator.setOnSetup).toHaveBeenCalledOnce();
expect(replicator.clearOnSetup).toHaveBeenCalledOnce();
});
});