Compose replication lifecycle without a legacy module

This commit is contained in:
vorotamoroz
2026-08-30 10:23:04 +00:00
parent a5756503a2
commit 72f033fca4
14 changed files with 939 additions and 937 deletions
@@ -0,0 +1,126 @@
import { describe, expect, it, vi } from "vitest";
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { VER, type EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { promiseWithResolvers } from "octagonal-wheels/promises";
import { useReplicationFeature } from "./index";
type BooleanHandler = (showMessage: boolean) => Promise<boolean>;
type ParseHandler = (documents: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<boolean>;
function setup(onCloseActiveReplication = vi.fn(async () => true)) {
const read = vi.fn(async () => new Uint8Array([1]));
const dispose = vi.fn(async () => undefined);
const createRemoteResource = vi.fn(async () => ({ read, dispose }));
const beforeReplicateHandlers = new Map<number, BooleanHandler>();
const centralRemoteHandlers: BooleanHandler[] = [];
let parseHandler: ParseHandler | undefined;
const services = {
API: { isMobile: vi.fn(() => false), isOnline: true },
appLifecycle: {
getUnresolvedMessages: { addHandler: vi.fn() },
isReady: true,
isSuspended: vi.fn(() => false),
onSettingLoaded: { addHandler: vi.fn() },
},
context: createServiceContext(),
databaseEvents: { onDatabaseInitialised: { addHandler: vi.fn() } },
keyValueDB: {
kvDB: {
get: vi.fn(async () => undefined),
set: vi.fn(async () => undefined),
},
},
path: { getPath: vi.fn((entry: { path: string }) => entry.path) },
replication: {
onBeforeReplicate: {
addHandler: vi.fn((handler: BooleanHandler, priority = 0) => {
beforeReplicateHandlers.set(priority, handler);
}),
},
onPrepareCentralRemoteReplication: {
addHandler: vi.fn((handler: BooleanHandler) => centralRemoteHandlers.push(handler)),
},
onReplicationFailed: { addHandler: vi.fn() },
parseSynchroniseResult: {
addHandler: vi.fn((handler: ParseHandler) => {
parseHandler = handler;
}),
},
replicateUnattendedByEvent: vi.fn(async () => ({ status: "completed" as const })),
},
replicator: {
createRemoteResource,
onBeforeReplicatorPublication: { addHandler: vi.fn() },
onCloseActiveReplication,
},
setting: { currentSettings: vi.fn(() => ({})) },
tweakValue: {},
vault: {},
};
const core = {
confirm: {},
localDatabase: {},
rebuilder: {},
services,
};
useReplicationFeature(core as never);
return {
beforeReplicateHandlers,
centralRemoteHandlers,
createRemoteResource,
dispose,
get parseHandler() {
return parseHandler;
},
onCloseActiveReplication,
read,
};
}
describe("replication serviceFeature composition", () => {
it("refreshes and disposes the remote Security Seed before central replication", async () => {
const { centralRemoteHandlers, createRemoteResource, dispose, read } = setup();
await expect(centralRemoteHandlers[0](false)).resolves.toBe(true);
expect(createRemoteResource).toHaveBeenCalledWith("security-seed", {});
expect(read).toHaveBeenCalledOnce();
expect(dispose).toHaveBeenCalledOnce();
});
it("keeps generic preflight separate from central-remote preparation", async () => {
const { beforeReplicateHandlers, centralRemoteHandlers, createRemoteResource } = setup();
const online = beforeReplicateHandlers.get(10);
const general = beforeReplicateHandlers.get(100);
expect(online).toBeDefined();
expect(general).toBeDefined();
expect(centralRemoteHandlers).toHaveLength(1);
await expect(online!(false)).resolves.toBe(true);
await expect(general!(false)).resolves.toBe(true);
expect(createRemoteResource).not.toHaveBeenCalled();
await expect(centralRemoteHandlers[0](false)).resolves.toBe(true);
expect(createRemoteResource).toHaveBeenCalledOnce();
});
it("requests owner retirement without awaiting the transition from result application", async () => {
const retirement = promiseWithResolvers<boolean>();
const onCloseActiveReplication = vi.fn(() => retirement.promise);
const harness = setup(onCloseActiveReplication);
const versionInfo = {
_id: "versioninfo",
_rev: "1-test",
type: "versioninfo",
version: VER + 1,
} as unknown as PouchDB.Core.ExistingDocument<EntryDoc>;
expect(harness.parseHandler).toBeDefined();
await expect(harness.parseHandler!([versionInfo])).resolves.toBe(true);
expect(onCloseActiveReplication).toHaveBeenCalledOnce();
retirement.resolve(true);
});
});