refactor: compose Journal remotes generically

This commit is contained in:
vorotamoroz
2026-08-01 06:09:02 +00:00
parent 133ef34d7b
commit 7955ab54d3
8 changed files with 108 additions and 12 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ import { AbstractModule } from "./modules/AbstractModule";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess";
import { ModuleReplicator } from "./modules/core/ModuleReplicator";
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB";
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO";
import { ModuleReplicatorJournal } from "./modules/core/ModuleReplicatorJournal";
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker";
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver";
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks";
@@ -139,7 +139,7 @@ export class LiveSyncBaseCore<
public registerModules(extraModules: AbstractModule[] = []) {
this._registerModule(new ModuleLiveSyncMain(this));
this._registerModule(new ModuleConflictChecker(this));
this._registerModule(new ModuleReplicatorMinIO(this));
this._registerModule(new ModuleReplicatorJournal(this));
this._registerModule(new ModuleReplicatorCouchDB(this));
this._registerModule(new ModuleReplicator(this));
this._registerModule(new ModuleConflictResolver(this));
+2 -2
View File
@@ -6,7 +6,7 @@ import {
type FilePathWithPrefix,
type ObsidianLiveSyncSettings,
REMOTE_COUCHDB,
REMOTE_MINIO,
isJournalRemoteType,
type EntryMilestoneInfo,
type EntryDoc,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
@@ -61,7 +61,7 @@ async function verifyRemoteState(
return false;
}
milestone = await dbRet.db.get(MILESTONE_DOCID);
} else if (settings.remoteType === REMOTE_MINIO) {
} else if (isJournalRemoteType(settings.remoteType)) {
const journalReplicator = replicator as LiveSyncJournalReplicator;
if (journalProtocolConfigurationForSettings(settings).journalFormat === "adaptive-v1") {
try {
@@ -7,6 +7,7 @@ import {
REMOTE_COUCHDB,
REMOTE_MINIO,
REMOTE_P2P,
REMOTE_WEBDAV,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { runCommand } from "./runCommand";
@@ -750,6 +751,32 @@ describe("runCommand abnormal cases", () => {
);
});
it("uses the Adaptive verification path for a WebDAV remote", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
settings.remoteType = REMOTE_WEBDAV;
settings.webDAVactiveConnectionURI = "sls+webdav://dav.example/dav";
settings.journalFormat = "adaptive-v1";
settings.packReadPolicy = "whole-pack";
const ensureCheckpointCachesAreFresh = vi.fn(async () => {});
core.services.replicator.getActiveReplicator.mockReturnValue({
nodeid: "test-node-id",
initializeDatabaseForReplication: vi.fn(async () => {}),
client: {
ensureCheckpointCachesAreFresh,
},
});
const result = await runCommand(makeOptions("mark-resolved", []), {
...context,
core,
});
expect(result).toBe(true);
expect(ensureCheckpointCachesAreFresh).toHaveBeenCalledTimes(1);
});
it("mark-resolved with remote-id temporarily activates it and runs markResolved", async () => {
const core = createCoreMock();
const settings = core.services.setting.currentSettings();
+3 -3
View File
@@ -1,5 +1,5 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { isJournalRemoteType, REMOTE_P2P, type RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncAbstractReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "@/modules/AbstractModule";
@@ -9,7 +9,7 @@ export class ModuleReplicatorCouchDB extends AbstractModule {
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
const settings = { ...this.settings, ...settingOverride };
// If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve.
if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) {
if (isJournalRemoteType(settings.remoteType) || settings.remoteType == REMOTE_P2P) {
return Promise.resolve(false);
}
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
@@ -17,7 +17,7 @@ export class ModuleReplicatorCouchDB extends AbstractModule {
_everyAfterResumeProcess(): Promise<boolean> {
if (this.services.appLifecycle.isSuspended()) return Promise.resolve(true);
if (!this.services.appLifecycle.isReady()) return Promise.resolve(true);
if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
if (!isJournalRemoteType(this.settings.remoteType) && this.settings.remoteType != REMOTE_P2P) {
const LiveSyncEnabled = this.settings.liveSync;
const continuous = LiveSyncEnabled;
const eventualOnStart = !LiveSyncEnabled && this.settings.syncOnStart;
@@ -1,7 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import { REMOTE_WEBDAV } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { ModuleReplicatorCouchDB } from "./ModuleReplicatorCouchDB.ts";
function createModule(settings: { liveSync: boolean; syncOnStart: boolean }, isReplicationReady = true) {
function createModule(
settings: { liveSync: boolean; remoteType?: string; syncOnStart: boolean },
isReplicationReady = true
) {
const openReplication = vi.fn(async () => true);
const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task());
const services = {
@@ -86,4 +90,19 @@ describe("ModuleReplicatorCouchDB resume replication activity", () => {
expect(runFiniteReplicationActivity).not.toHaveBeenCalled();
expect(openReplication).not.toHaveBeenCalled();
});
it("does not claim or resume WebDAV as CouchDB", async () => {
const { module, openReplication, runFiniteReplicationActivity } = createModule({
liveSync: true,
remoteType: REMOTE_WEBDAV,
syncOnStart: true,
});
await expect(module._anyNewReplicator()).resolves.toBe(false);
await module._everyAfterResumeProcess();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(runFiniteReplicationActivity).not.toHaveBeenCalled();
expect(openReplication).not.toHaveBeenCalled();
});
});
@@ -1,13 +1,13 @@
import { REMOTE_MINIO, type RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { isJournalRemoteType, type RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
import type { LiveSyncAbstractReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/LiveSyncAbstractReplicator";
import type { LiveSyncCore } from "@/main";
import { AbstractModule } from "@/modules/AbstractModule";
export class ModuleReplicatorMinIO extends AbstractModule {
export class ModuleReplicatorJournal extends AbstractModule {
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_MINIO) {
if (isJournalRemoteType(settings.remoteType)) {
return Promise.resolve(new LiveSyncJournalReplicator(this.core));
}
return Promise.resolve(false);
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";
import { REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_WEBDAV } from "@vrtmrz/livesync-commonlib/compat/common/types";
const journalReplicatorConstructor = vi.hoisted(() => vi.fn());
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator", () => ({
LiveSyncJournalReplicator: class {
constructor(env: unknown) {
journalReplicatorConstructor(env);
}
},
}));
import { ModuleReplicatorJournal } from "./ModuleReplicatorJournal.ts";
function createModule(remoteType: string) {
const services = {
API: {
addCommand: vi.fn(),
addLog: vi.fn(),
addRibbonIcon: vi.fn(),
registerProtocolHandler: vi.fn(),
registerWindow: vi.fn(),
},
};
const core = {
_services: services,
services,
settings: { remoteType },
} as any;
return { core, module: new ModuleReplicatorJournal(core) };
}
describe("ModuleReplicatorJournal", () => {
it.each([REMOTE_MINIO, REMOTE_WEBDAV])("creates a Journal replicator for %s", async (remoteType) => {
const { core, module } = createModule(remoteType);
await expect(module._anyNewReplicator()).resolves.not.toBe(false);
expect(journalReplicatorConstructor).toHaveBeenLastCalledWith(core);
});
it("does not claim a CouchDB remote", async () => {
const { module } = createModule(REMOTE_COUCHDB);
await expect(module._anyNewReplicator()).resolves.toBe(false);
});
});
@@ -83,7 +83,10 @@ function suggestRemoteConfigurationName(parsed: RemoteConfigurationResult): stri
if (parsed.type === "s3") {
return `S3 ${parsed.settings.bucket || parsed.settings.endpoint}`;
}
return `P2P ${parsed.settings.P2P_roomID || "Remote"}`;
if (parsed.type === "p2p") {
return `P2P ${parsed.settings.P2P_roomID || "Remote"}`;
}
return "WebDAV Remote";
}
export function paneRemoteConfig(