mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-21 18:17:05 +00:00
Migrate LiveSync flows to provider-owned resources
This commit is contained in:
@@ -134,8 +134,8 @@ export class ModuleReplicator extends AbstractModule {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
_onReplicatorInitialised(): Promise<boolean> {
|
||||
// For now, we only need to clear the error related to replicator initialisation, but in the future, if there are more things to do when the replicator is initialised, we can add them here.
|
||||
_onBeforeReplicatorPublication(): Promise<boolean> {
|
||||
// Clear key-derivation handlers before the candidate Replicator becomes active.
|
||||
clearHandlers();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
@@ -347,7 +347,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
||||
// }
|
||||
|
||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.replicator.onReplicatorInitialised.addHandler(this._onReplicatorInitialised.bind(this));
|
||||
services.replicator.onBeforeReplicatorPublication.addHandler(this._onBeforeReplicatorPublication.bind(this));
|
||||
services.databaseEvents.onDatabaseInitialised.addHandler(this._everyOnDatabaseInitialized.bind(this));
|
||||
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
|
||||
services.replication.parseSynchroniseResult.addHandler(this._parseReplicationResult.bind(this));
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("ModuleReplicator", () => {
|
||||
const services = {
|
||||
API: { isOnline: true },
|
||||
replicator: {
|
||||
onReplicatorInitialised: { addHandler: vi.fn() },
|
||||
onBeforeReplicatorPublication: { addHandler: vi.fn() },
|
||||
getActiveReplicator: () => ({ ensurePBKDF2Salt }),
|
||||
},
|
||||
setting: { currentSettings: () => ({}) },
|
||||
@@ -45,7 +45,7 @@ describe("ModuleReplicator", () => {
|
||||
showError: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
},
|
||||
_onReplicatorInitialised: vi.fn(),
|
||||
_onBeforeReplicatorPublication: vi.fn(),
|
||||
_everyOnDatabaseInitialized: vi.fn(),
|
||||
_everyOnloadAfterLoadSettings: vi.fn(),
|
||||
_parseReplicationResult: vi.fn(),
|
||||
@@ -71,7 +71,7 @@ describe("ModuleReplicator", () => {
|
||||
const services = {
|
||||
API: { isOnline: true },
|
||||
replicator: {
|
||||
onReplicatorInitialised: { addHandler: vi.fn() },
|
||||
onBeforeReplicatorPublication: { addHandler: vi.fn() },
|
||||
getActiveReplicator: () => ({ ensurePBKDF2Salt }),
|
||||
},
|
||||
setting: { currentSettings: () => ({}) },
|
||||
@@ -94,7 +94,7 @@ describe("ModuleReplicator", () => {
|
||||
showError: vi.fn(),
|
||||
clearError: vi.fn(),
|
||||
},
|
||||
_onReplicatorInitialised: vi.fn(),
|
||||
_onBeforeReplicatorPublication: vi.fn(),
|
||||
_everyOnDatabaseInitialized: vi.fn(),
|
||||
_everyOnloadAfterLoadSettings: vi.fn(),
|
||||
_parseReplicationResult: vi.fn(),
|
||||
|
||||
@@ -20,6 +20,8 @@ import { $msg, translateIfAvailable } from "@/common/translation";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import { withOwnedRemoteResource } from "@/common/ownedRemoteResource";
|
||||
import { REMOTE_RESOURCE_KINDS } from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
/**
|
||||
* Localised counterpart of Commonlib's `confName()`, which takes no translator.
|
||||
@@ -271,12 +273,15 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
|
||||
|
||||
async _fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise<RemotePreferredTweakResult> {
|
||||
try {
|
||||
const replicator = await this.services.replicator.getNewReplicator(trialSetting);
|
||||
if (!replicator) {
|
||||
const probe = await this.services.replicator.createRemoteResource(
|
||||
REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK,
|
||||
trialSetting
|
||||
);
|
||||
if (!probe) {
|
||||
this._log("The remote type does not support preferred tweak values.", LOG_LEVEL_NOTICE);
|
||||
return { status: RemotePreferredTweakStatuses.UNSUPPORTED };
|
||||
}
|
||||
return await replicator.getRemotePreferredTweakValues(trialSetting);
|
||||
return await withOwnedRemoteResource(probe, (ownedProbe) => ownedProbe.read());
|
||||
} catch (ex) {
|
||||
this._log("Failed to get the preferred tweak values from the remote.", LOG_LEVEL_NOTICE);
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { ModuleResolvingMismatchedTweaks } from "./ModuleResolveMismatchedTweaks";
|
||||
import { setLang } from "@/common/translation";
|
||||
import { REMOTE_RESOURCE_KINDS } from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
|
||||
const askSelectStringDialogue = vi.fn(async (..._args: unknown[]): Promise<string | undefined> => undefined);
|
||||
@@ -57,27 +58,31 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
|
||||
describe("ModuleResolvingMismatchedTweaks", () => {
|
||||
it("returns an unconfigured remote result without a separate connection preflight", async () => {
|
||||
const { module, core } = createModule();
|
||||
const tryConnectRemote = vi.fn(async () => true);
|
||||
const getRemotePreferredTweakValues = vi.fn(async () => ({
|
||||
const read = vi.fn(async () => ({
|
||||
status: "not-configured" as const,
|
||||
reason: "milestone-missing" as const,
|
||||
}));
|
||||
const dispose = vi.fn(async () => undefined);
|
||||
const createRemoteResource = vi.fn(async () => ({ read, dispose }));
|
||||
core._services.replicator = {
|
||||
getNewReplicator: vi.fn(async () => ({ tryConnectRemote, getRemotePreferredTweakValues })),
|
||||
createRemoteResource,
|
||||
getNewReplicator: vi.fn(() => Promise.reject(new Error("must not borrow a Replicator"))),
|
||||
};
|
||||
|
||||
await expect(module._fetchRemotePreferredTweakValues(core.settings)).resolves.toEqual({
|
||||
status: "not-configured",
|
||||
reason: "milestone-missing",
|
||||
});
|
||||
expect(getRemotePreferredTweakValues).toHaveBeenCalledOnce();
|
||||
expect(tryConnectRemote).not.toHaveBeenCalled();
|
||||
expect(createRemoteResource).toHaveBeenCalledWith(REMOTE_RESOURCE_KINDS.PREFERRED_TWEAK, core.settings);
|
||||
expect(read).toHaveBeenCalledOnce();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
expect(core._services.replicator.getNewReplicator).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns unsupported when no replicator implements the remote type", async () => {
|
||||
const { module, core } = createModule();
|
||||
core._services.replicator = {
|
||||
getNewReplicator: vi.fn(async () => undefined),
|
||||
createRemoteResource: vi.fn(async () => undefined),
|
||||
};
|
||||
|
||||
await expect(module._fetchRemotePreferredTweakValues(core.settings)).resolves.toEqual({
|
||||
@@ -85,6 +90,26 @@ describe("ModuleResolvingMismatchedTweaks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("disposes the preferred-tweak probe when reading fails", async () => {
|
||||
const { module, core } = createModule();
|
||||
const error = new Error("remote unavailable");
|
||||
const dispose = vi.fn(async () => undefined);
|
||||
core._services.replicator = {
|
||||
createRemoteResource: vi.fn(async () => ({
|
||||
read: vi.fn(async () => {
|
||||
throw error;
|
||||
}),
|
||||
dispose,
|
||||
})),
|
||||
};
|
||||
|
||||
await expect(module._fetchRemotePreferredTweakValues(core.settings)).resolves.toEqual({
|
||||
status: "unavailable",
|
||||
error,
|
||||
});
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should enable and auto-accept compatible mismatches when the preference is undefined", async () => {
|
||||
const { module, core, askSelectStringDialogue, applyPartial } = createModule({
|
||||
autoAcceptCompatibleTweak: undefined,
|
||||
|
||||
@@ -13,11 +13,10 @@ import {
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { delay, isObjectDifferent, sizeToHumanReadable } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import { checkSyncInfo } from "@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation";
|
||||
import { testCrypt } from "octagonal-wheels/encryption/encryption";
|
||||
import ObsidianLiveSyncPlugin from "@/main.ts";
|
||||
import { scheduleTask } from "@/common/utils.ts";
|
||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { REMOTE_RESOURCE_KINDS } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import {
|
||||
type AllSettingItemKey,
|
||||
type AllStringItemKey,
|
||||
@@ -78,6 +77,7 @@ import type {
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
import { SetupManager } from "@/modules/features/SetupManager.ts";
|
||||
import { isP2PMainRemote } from "@/common/remoteConfiguration.ts";
|
||||
import { withOwnedRemoteResource } from "@/common/ownedRemoteResource.ts";
|
||||
|
||||
// For creating a document
|
||||
// const toc = new Set<string>();
|
||||
@@ -340,15 +340,18 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
async testConnection(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<void> {
|
||||
const trialSetting = { ...this.editingSettings, ...settingOverride };
|
||||
const replicator = await this.services.replicator.getNewReplicator(trialSetting);
|
||||
if (!replicator) {
|
||||
Logger("No replicator available for the current settings.", LOG_LEVEL_NOTICE);
|
||||
const probe = await this.services.replicator.createRemoteResource(
|
||||
REMOTE_RESOURCE_KINDS.CONNECTION,
|
||||
trialSetting
|
||||
);
|
||||
if (!probe) {
|
||||
Logger("Connection testing is unavailable for the current settings.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
await replicator.tryConnectRemote(trialSetting);
|
||||
const status = await replicator.getRemoteStatus(trialSetting);
|
||||
if (status) {
|
||||
if (status.estimatedSize) {
|
||||
await withOwnedRemoteResource(probe, async (ownedProbe) => {
|
||||
await ownedProbe.check({ createIfMissing: true, showResult: true });
|
||||
const status = await ownedProbe.getStatus();
|
||||
if (status && status.estimatedSize) {
|
||||
Logger(
|
||||
$msg("obsidianLiveSyncSettingTab.logEstimatedSize", {
|
||||
size: sizeToHumanReadable(status.estimatedSize),
|
||||
@@ -356,7 +359,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeSetting() {
|
||||
@@ -954,27 +957,23 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
visibility:
|
||||
this.isConfiguredAs("remoteType", REMOTE_COUCHDB) || this.isConfiguredAs("remoteType", REMOTE_MINIO),
|
||||
}) as OnUpdateResult;
|
||||
// E2EE Function
|
||||
/**
|
||||
* Checks the edited CouchDB passphrase through an owned synchronisation-
|
||||
* information resource. A missing document may be created by the check.
|
||||
*/
|
||||
checkWorkingPassphrase = async (): Promise<boolean> => {
|
||||
if (this.editingSettings.remoteType == REMOTE_MINIO) return true;
|
||||
|
||||
const settingForCheck: RemoteDBSettings = {
|
||||
...this.editingSettings,
|
||||
};
|
||||
const replicator = this.services.replicator.getNewReplicator(settingForCheck);
|
||||
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return true;
|
||||
|
||||
const db = await replicator.connectRemoteCouchDBWithSetting(
|
||||
settingForCheck,
|
||||
this.services.API.isMobile(),
|
||||
true
|
||||
const resource = await this.services.replicator.createRemoteResource(
|
||||
REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION,
|
||||
settingForCheck
|
||||
);
|
||||
if (typeof db === "string") {
|
||||
Logger($msg("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
if (!resource) return true;
|
||||
try {
|
||||
if (await checkSyncInfo(db.db)) {
|
||||
if (await resource.check()) {
|
||||
// Logger($msg("obsidianLiveSyncSettingTab.logDatabaseConnected"), LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
} else {
|
||||
@@ -982,7 +981,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
await db.db.close();
|
||||
await resource.dispose();
|
||||
}
|
||||
};
|
||||
isPassphraseValid = async () => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { REMOTE_RESOURCE_KINDS } from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
const negotiationMocks = vi.hoisted(() => ({
|
||||
checkSyncInfo: vi.fn(async () => true),
|
||||
}));
|
||||
const settingsInitialisationMocks = vi.hoisted(() => ({
|
||||
applySettingsWithInitialisationChoice: vi.fn(),
|
||||
}));
|
||||
@@ -38,10 +36,6 @@ vi.mock("@/common/events.ts", () => ({
|
||||
eventHub: { emitEvent: vi.fn(), onEvent: vi.fn() },
|
||||
}));
|
||||
vi.mock("@/modules/features/SetupManager.ts", () => ({ SetupManager: class {} }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation", () => negotiationMocks);
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||
LiveSyncCouchDBReplicator: class {},
|
||||
}));
|
||||
vi.mock("./LiveSyncSetting.ts", () => ({ LiveSyncSetting: class {} }));
|
||||
vi.mock("./SettingPane.ts", () => ({
|
||||
enableOnly: vi.fn(() => vi.fn()),
|
||||
@@ -63,7 +57,6 @@ vi.mock("./PanePowerUsers.ts", () => ({ panePowerUsers: vi.fn() }));
|
||||
vi.mock("./PanePatches.ts", () => ({ panePatches: vi.fn() }));
|
||||
vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
|
||||
|
||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,19 +64,15 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||
it("closes the finite remote connection after checking synchronisation information", async () => {
|
||||
const remoteDatabase = {
|
||||
close: vi.fn(async () => undefined),
|
||||
};
|
||||
const replicator = Object.assign(new LiveSyncCouchDBReplicator({} as never), {
|
||||
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase })),
|
||||
});
|
||||
it("awaits and disposes the owned synchronisation-information resource", async () => {
|
||||
const check = vi.fn(async () => true);
|
||||
const dispose = vi.fn(async () => undefined);
|
||||
const createRemoteResource = vi.fn(async () => ({ check, dispose }));
|
||||
const plugin = {
|
||||
app: {},
|
||||
core: {
|
||||
services: {
|
||||
API: { isMobile: vi.fn(() => false) },
|
||||
replicator: { getNewReplicator: vi.fn(() => replicator) },
|
||||
replicator: { createRemoteResource },
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -97,8 +86,76 @@ describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||
|
||||
await expect(tab.checkWorkingPassphrase()).resolves.toBe(true);
|
||||
|
||||
expect(negotiationMocks.checkSyncInfo).toHaveBeenCalledWith(remoteDatabase);
|
||||
expect(remoteDatabase.close).toHaveBeenCalledOnce();
|
||||
expect(createRemoteResource).toHaveBeenCalledWith(
|
||||
REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION,
|
||||
expect.objectContaining({ remoteType: REMOTE_COUCHDB })
|
||||
);
|
||||
expect(check).toHaveBeenCalledOnce();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not use the general Replicator factory solely to verify synchronisation information", async () => {
|
||||
const getNewReplicator = vi.fn(() => Promise.reject(new Error("must not construct a Replicator")));
|
||||
const createRemoteResource = vi.fn(async () => ({
|
||||
check: vi.fn(async () => true),
|
||||
dispose: vi.fn(async () => undefined),
|
||||
}));
|
||||
const plugin = {
|
||||
app: {},
|
||||
core: {
|
||||
services: {
|
||||
replicator: { createRemoteResource, getNewReplicator },
|
||||
},
|
||||
},
|
||||
};
|
||||
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||
Object.assign(tab, {
|
||||
_editingSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(tab.checkWorkingPassphrase()).resolves.toBe(true);
|
||||
|
||||
expect(getNewReplicator).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab connection testing", () => {
|
||||
it("uses and disposes the flow-specific connection probe without borrowing a Replicator", async () => {
|
||||
const check = vi.fn(async () => ({ ok: true as const }));
|
||||
const getStatus = vi.fn(async () => ({ estimatedSize: 1024 }));
|
||||
const dispose = vi.fn(async () => undefined);
|
||||
const createRemoteResource = vi.fn(async () => ({ check, getStatus, dispose }));
|
||||
const getNewReplicator = vi.fn(() => Promise.reject(new Error("must not borrow a Replicator")));
|
||||
const plugin = {
|
||||
app: {},
|
||||
core: {
|
||||
services: {
|
||||
replicator: { createRemoteResource, getNewReplicator },
|
||||
},
|
||||
},
|
||||
};
|
||||
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||
Object.assign(tab, {
|
||||
_editingSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
couchDB_DBNAME: "saved",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(tab.testConnection({ couchDB_DBNAME: "trial" })).resolves.toBeUndefined();
|
||||
|
||||
expect(createRemoteResource).toHaveBeenCalledWith(
|
||||
REMOTE_RESOURCE_KINDS.CONNECTION,
|
||||
expect.objectContaining({ remoteType: REMOTE_COUCHDB, couchDB_DBNAME: "trial" })
|
||||
);
|
||||
expect(check).toHaveBeenCalledWith({ createIfMissing: true, showResult: true });
|
||||
expect(getStatus).toHaveBeenCalledOnce();
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
expect(getNewReplicator).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
import { copyTo, pickBucketSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { TYPE_CANCELLED, type SetupRemoteBucketResultType } from "./setupDialogTypes";
|
||||
import { $msg as translateMessage } from "@/common/translation";
|
||||
import { withOwnedRemoteResource } from "@/common/ownedRemoteResource";
|
||||
import { REMOTE_RESOURCE_KINDS } from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS);
|
||||
|
||||
@@ -81,13 +83,18 @@
|
||||
try {
|
||||
processing = true;
|
||||
const trialRemoteSetting = generateSetting();
|
||||
const replicator = await context.services.replicator.getNewReplicator(trialRemoteSetting);
|
||||
if (!replicator) {
|
||||
return translateMessage("Failed to create replicator instance.");
|
||||
const probe = await context.services.replicator.createRemoteResource(
|
||||
REMOTE_RESOURCE_KINDS.CONNECTION,
|
||||
trialRemoteSetting
|
||||
);
|
||||
if (!probe) {
|
||||
return translateMessage("Failed to connect to the server. Please check your settings.");
|
||||
}
|
||||
try {
|
||||
const result = await replicator.tryConnectRemote(trialRemoteSetting, false);
|
||||
if (result) {
|
||||
const result = await withOwnedRemoteResource(probe, (ownedProbe) =>
|
||||
ownedProbe.check({ createIfMissing: true, showResult: false })
|
||||
);
|
||||
if (result.ok) {
|
||||
return "";
|
||||
} else {
|
||||
return translateMessage("Failed to connect to the server. Please check your settings.");
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
} from "./setupDialogTypes";
|
||||
import { isValidCouchDBServerURL, probeCouchDBConnection } from "./couchDBConnectionProbe";
|
||||
import { $msg as translateMessage } from "@/common/translation";
|
||||
import { REMOTE_RESOURCE_KINDS } from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
const default_setting = pickCouchDBSyncSettings(DEFAULT_SETTINGS);
|
||||
|
||||
@@ -73,16 +74,15 @@
|
||||
try {
|
||||
processing = true;
|
||||
const trialRemoteSetting = generateSetting();
|
||||
const replicator = await context.services.replicator.getNewReplicator(trialRemoteSetting);
|
||||
if (!replicator) {
|
||||
return translateMessage("Failed to create replicator instance.");
|
||||
const probe = await context.services.replicator.createRemoteResource(
|
||||
REMOTE_RESOURCE_KINDS.CONNECTION,
|
||||
trialRemoteSetting
|
||||
);
|
||||
if (!probe) {
|
||||
return translateMessage("Failed to connect to the server. Please check your settings.");
|
||||
}
|
||||
try {
|
||||
const result = await probeCouchDBConnection(
|
||||
replicator,
|
||||
trialRemoteSetting,
|
||||
setupMode === "create-or-connect"
|
||||
);
|
||||
const result = await probeCouchDBConnection(probe, setupMode === "create-or-connect");
|
||||
if (result.ok) {
|
||||
return "";
|
||||
} else {
|
||||
|
||||
@@ -1,60 +1,14 @@
|
||||
import type {
|
||||
ObsidianLiveSyncSettings,
|
||||
RemoteDBSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
|
||||
|
||||
export type CouchDBConnectionProbeResult = { ok: true } | { ok: false; reason: string };
|
||||
|
||||
type CouchDBConnectionResult =
|
||||
| string
|
||||
| {
|
||||
db: { close(): Promise<void> };
|
||||
info: unknown;
|
||||
};
|
||||
|
||||
export interface CouchDBConnectionProbe {
|
||||
isMobile(): boolean;
|
||||
connectRemoteCouchDBWithSetting(
|
||||
settings: RemoteDBSettings,
|
||||
isMobile: boolean,
|
||||
performSetup: boolean,
|
||||
skipInfo: boolean
|
||||
): CouchDBConnectionResult | Promise<CouchDBConnectionResult>;
|
||||
}
|
||||
|
||||
export function isCouchDBConnectionProbe(value: unknown): value is CouchDBConnectionProbe {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"isMobile" in value &&
|
||||
typeof value.isMobile === "function" &&
|
||||
"connectRemoteCouchDBWithSetting" in value &&
|
||||
typeof value.connectRemoteCouchDBWithSetting === "function"
|
||||
);
|
||||
}
|
||||
import type { RemoteConnectionProbe, RemoteConnectionProbeResult } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import { withOwnedRemoteResource } from "@/common/ownedRemoteResource";
|
||||
|
||||
/** Run the selected CouchDB setup mode within one owned probe lifetime. */
|
||||
export async function probeCouchDBConnection(
|
||||
replicator: unknown,
|
||||
settings: ObsidianLiveSyncSettings,
|
||||
probe: RemoteConnectionProbe,
|
||||
createIfMissing: boolean
|
||||
): Promise<CouchDBConnectionProbeResult> {
|
||||
if (!isCouchDBConnectionProbe(replicator)) {
|
||||
return { ok: false, reason: "The CouchDB connection probe is unavailable." };
|
||||
}
|
||||
const result = await replicator.connectRemoteCouchDBWithSetting(
|
||||
settings,
|
||||
replicator.isMobile(),
|
||||
createIfMissing,
|
||||
false
|
||||
): Promise<RemoteConnectionProbeResult> {
|
||||
return await withOwnedRemoteResource(probe, (ownedProbe) =>
|
||||
ownedProbe.check({ createIfMissing, showResult: false })
|
||||
);
|
||||
if (typeof result === "string") {
|
||||
return { ok: false, reason: result };
|
||||
}
|
||||
try {
|
||||
return { ok: true };
|
||||
} finally {
|
||||
await result.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidCouchDBServerURL(value: string): boolean {
|
||||
|
||||
@@ -1,47 +1,34 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
|
||||
import { isValidCouchDBServerURL, probeCouchDBConnection } from "./couchDBConnectionProbe";
|
||||
|
||||
const settings = {
|
||||
couchDB_URI: "https://couch.example",
|
||||
couchDB_DBNAME: "notes",
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
describe("CouchDB setup connection policy", () => {
|
||||
it.each([
|
||||
[false, "connect to an existing database"],
|
||||
[true, "create or connect to a database"],
|
||||
] as const)(
|
||||
"%s can %s without changing the Commonlib connection contract",
|
||||
async (createIfMissing, _description) => {
|
||||
const close = vi.fn(async () => undefined);
|
||||
const connectRemoteCouchDBWithSetting = vi.fn(async () => ({
|
||||
db: { close },
|
||||
info: { db_name: "notes" },
|
||||
}));
|
||||
const replicator = {
|
||||
isMobile: vi.fn(() => false),
|
||||
connectRemoteCouchDBWithSetting,
|
||||
tryConnectRemote: vi.fn(),
|
||||
};
|
||||
] as const)("%s can %s through an owned connection probe", async (createIfMissing, _description) => {
|
||||
const check = vi.fn(async () => ({ ok: true as const }));
|
||||
const dispose = vi.fn(async () => undefined);
|
||||
const probe = { check, getStatus: vi.fn(), dispose };
|
||||
|
||||
await expect(probeCouchDBConnection(replicator, settings, createIfMissing)).resolves.toEqual({ ok: true });
|
||||
expect(connectRemoteCouchDBWithSetting).toHaveBeenCalledWith(settings, false, createIfMissing, false);
|
||||
expect(replicator.tryConnectRemote).not.toHaveBeenCalled();
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
}
|
||||
);
|
||||
await expect(probeCouchDBConnection(probe, createIfMissing)).resolves.toEqual({ ok: true });
|
||||
|
||||
it("returns the connection error without saving or creating through another path", async () => {
|
||||
const replicator = {
|
||||
isMobile: vi.fn(() => true),
|
||||
connectRemoteCouchDBWithSetting: vi.fn(() => "database does not exist"),
|
||||
expect(check).toHaveBeenCalledWith({ createIfMissing, showResult: false });
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("returns a connection error and still disposes the probe", async () => {
|
||||
const dispose = vi.fn(async () => undefined);
|
||||
const probe = {
|
||||
check: vi.fn(async () => ({ ok: false as const, reason: "database does not exist" })),
|
||||
getStatus: vi.fn(),
|
||||
dispose,
|
||||
};
|
||||
|
||||
await expect(probeCouchDBConnection(replicator, settings, false)).resolves.toEqual({
|
||||
await expect(probeCouchDBConnection(probe, false)).resolves.toEqual({
|
||||
ok: false,
|
||||
reason: "database does not exist",
|
||||
});
|
||||
expect(dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
Reference in New Issue
Block a user