mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-06 16:28:18 +00:00
Merge main into CouchDB cache compatibility changes
This commit is contained in:
commit
c306146dcd
78 files changed
+6177
-929
No files matched your search
@@ -0,0 +1,15 @@
|
||||
import type { RemoteConnectionProbe } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import { withOwnedRemoteResource } from "./ownedRemoteResource";
|
||||
|
||||
/** Select initial settings from a read-only declaration before opening the local database. */
|
||||
export async function resolveInitialPathIdentity(
|
||||
probe: RemoteConnectionProbe | undefined,
|
||||
newEnvironmentVersion: number
|
||||
): Promise<0 | 1> {
|
||||
if (!probe) throw new Error("The remote path identity cannot be inspected.");
|
||||
return await withOwnedRemoteResource(probe, async (ownedProbe) => {
|
||||
if (!ownedProbe.inspectPathIdentity) throw new Error("The remote path identity cannot be inspected.");
|
||||
const result = await ownedProbe.inspectPathIdentity();
|
||||
return result.status === "empty" ? (newEnvironmentVersion === 1 ? 1 : 0) : result.pathNormalizationVersion;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { PathIdentityInspection, RemoteConnectionProbe } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import { resolveInitialPathIdentity } from "./initialPathIdentity";
|
||||
|
||||
function makeProbe(result: PathIdentityInspection) {
|
||||
return {
|
||||
check: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
inspectPathIdentity: vi.fn(async () => result),
|
||||
dispose: vi.fn(async () => undefined),
|
||||
} satisfies RemoteConnectionProbe;
|
||||
}
|
||||
|
||||
describe("resolveInitialPathIdentity", () => {
|
||||
it.each([0, 1] as const)("adopts existing version %s instead of the new-environment default", async (version) => {
|
||||
const probe = makeProbe({ status: "configured", pathNormalizationVersion: version });
|
||||
await expect(resolveInitialPathIdentity(probe, 1 - version)).resolves.toBe(version);
|
||||
expect(probe.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
it.each([0, 1] as const)("retains version %s only for a confirmed empty remote", async (version) => {
|
||||
const probe = makeProbe({ status: "empty" });
|
||||
await expect(resolveInitialPathIdentity(probe, version)).resolves.toBe(version);
|
||||
expect(probe.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("preserves inspection failures and disposes the probe", async () => {
|
||||
const probe = makeProbe({ status: "empty" });
|
||||
const error = new Error("remote unavailable");
|
||||
probe.inspectPathIdentity.mockRejectedValue(error);
|
||||
await expect(resolveInitialPathIdentity(probe, 1)).rejects.toBe(error);
|
||||
expect(probe.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("refuses an unavailable inspection capability without guessing", async () => {
|
||||
const probe = makeProbe({ status: "empty" });
|
||||
const { inspectPathIdentity: _inspect, ...unsupported } = probe;
|
||||
await expect(resolveInitialPathIdentity(unsupported, 1)).rejects.toThrow("cannot be inspected");
|
||||
expect(probe.dispose).toHaveBeenCalledOnce();
|
||||
await expect(resolveInitialPathIdentity(undefined, 1)).rejects.toThrow("cannot be inspected");
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,9 @@
|
||||
* remove it from this map in the same change.
|
||||
*/
|
||||
export const liveSyncProvisionalEnglishMessages = {
|
||||
"File identity": "File identity",
|
||||
"Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild from a complete authoritative Vault and Fetch on the other devices. With P2P, Rebuild prepares only the source device's local database. Resolve colliding file names first.":
|
||||
"Version 0 preserves legacy path identity; version 1 uses Unicode NFC. Physical file names stay unchanged. Update every device and stop synchronisation, then Rebuild from a complete authoritative Vault and Fetch on the other devices. With P2P, Rebuild prepares only the source device's local database. Resolve colliding file names first.",
|
||||
"Synchronisation protection": "Synchronisation protection",
|
||||
"Synchronising your Vault": "Synchronising your Vault",
|
||||
"Receiving changes from the server…": "Receiving changes from the server…",
|
||||
|
||||
@@ -3167,7 +3167,7 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
"zh-tw": "若啟用「自動啟動 P2P 連線」,外掛啟動時會自動建立 P2P 連線。",
|
||||
},
|
||||
"If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": {
|
||||
def: "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).",
|
||||
def: "When enabled, chunks are split on the UI thread (the previous behaviour).",
|
||||
es: "Si se desactiva, los chunks se dividen en el hilo de UI (comportamiento anterior)",
|
||||
fr: "Si désactivé, les fragments seront découpés sur le thread UI (comportement précédent).",
|
||||
he: "אם מכובה, נתחים יפוצלו בשרשור ממשק המשתמש (התנהגות קודמת).",
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"I understand the risks and will proceed without a backup.": "I understand the risks and will proceed without a backup.",
|
||||
"I Understand, Overwrite Server": "I Understand, Overwrite Server",
|
||||
"If \"Auto Start P2P Connection\" is enabled, the P2P connection will be started automatically when the plug-in launches.": "If \"Auto Start P2P Connection\" is enabled, the P2P connection will be started automatically when the plug-in launches.",
|
||||
"If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).",
|
||||
"If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": "When enabled, chunks are split on the UI thread (the previous behaviour).",
|
||||
"If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.",
|
||||
"If enabled, changes will be automatically broadcasted to all connected peers. Notified peers will start fetching the changes.": "If enabled, changes will be automatically broadcasted to all connected peers. Notified peers will start fetching the changes.",
|
||||
"If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.",
|
||||
|
||||
@@ -437,8 +437,7 @@ If "Auto Start P2P Connection" is enabled, the P2P connection will be started au
|
||||
If "Auto Start P2P Connection" is enabled, the P2P connection will be
|
||||
started automatically when the plug-in launches.
|
||||
If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).:
|
||||
If disabled(toggled), chunks will be split on the UI thread (Previous
|
||||
behaviour).
|
||||
When enabled, chunks are split on the UI thread (the previous behaviour).
|
||||
If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.:
|
||||
If enabled per-filed efficient customization sync will be used. We need a
|
||||
small migration when enabling this. And all devices should be updated to
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createNewVaultSettings as createCommonlibNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
/** Create settings for a new LiveSync environment. */
|
||||
export function createNewVaultSettings(): ObsidianLiveSyncSettings {
|
||||
return {
|
||||
...createCommonlibNewVaultSettings(),
|
||||
pathNormalizationVersion: 1,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
NEW_VAULT_SETTINGS,
|
||||
SETTINGS_SCHEMA_DEFAULTS,
|
||||
createNewVaultSettings as createCommonlibNewVaultSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { createNewVaultSettings } from "./newVaultSettings.ts";
|
||||
|
||||
describe("createNewVaultSettings", () => {
|
||||
it("selects NFC path identity only for new host settings", () => {
|
||||
expect(SETTINGS_SCHEMA_DEFAULTS.pathNormalizationVersion).toBe(0);
|
||||
expect(NEW_VAULT_SETTINGS.pathNormalizationVersion).toBe(0);
|
||||
expect(createCommonlibNewVaultSettings().pathNormalizationVersion).toBe(0);
|
||||
expect(createNewVaultSettings().pathNormalizationVersion).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps remote configurations independent of shared and other new-Vault settings", () => {
|
||||
const first = createNewVaultSettings();
|
||||
const second = createNewVaultSettings();
|
||||
const shared = createCommonlibNewVaultSettings();
|
||||
|
||||
expect(first.remoteConfigurations).not.toBe(second.remoteConfigurations);
|
||||
expect(first.remoteConfigurations).not.toBe(shared.remoteConfigurations);
|
||||
expect(first.pluginSyncExtendedSetting).not.toBe(second.pluginSyncExtendedSetting);
|
||||
expect(first.pluginSyncExtendedSetting).not.toBe(shared.pluginSyncExtendedSetting);
|
||||
|
||||
first.remoteConfigurations.example = {
|
||||
id: "example",
|
||||
name: "Example",
|
||||
uri: "sls+http://localhost/",
|
||||
isEncrypted: false,
|
||||
};
|
||||
|
||||
expect(second.remoteConfigurations).toEqual({});
|
||||
expect(shared.remoteConfigurations).toEqual({});
|
||||
expect(NEW_VAULT_SETTINGS.remoteConfigurations).toEqual({});
|
||||
expect(NEW_VAULT_SETTINGS.pluginSyncExtendedSetting).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { LOG_LEVEL_NOTICE, REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { LOG_LEVEL_NOTICE, REMOTE_COUCHDB, REMOTE_MINIO, VERSIONING_DOCID } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
|
||||
@@ -21,6 +21,7 @@ const mocks = vi.hoisted(() => ({
|
||||
objectStorage: [] as Array<{
|
||||
host: unknown;
|
||||
tryConnectRemote: ReturnType<typeof vi.fn>;
|
||||
inspectRemotePathIdentity: ReturnType<typeof vi.fn>;
|
||||
getRemoteStatus: ReturnType<typeof vi.fn>;
|
||||
getRemotePreferredTweakValues: ReturnType<typeof vi.fn>;
|
||||
getReplicationPBKDF2Salt: ReturnType<typeof vi.fn>;
|
||||
@@ -64,6 +65,7 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalRe
|
||||
LiveSyncJournalReplicator: class {
|
||||
host: unknown;
|
||||
tryConnectRemote = vi.fn();
|
||||
inspectRemotePathIdentity = vi.fn();
|
||||
getRemoteStatus = vi.fn();
|
||||
getRemotePreferredTweakValues = vi.fn();
|
||||
getReplicationPBKDF2Salt = vi.fn();
|
||||
@@ -135,6 +137,45 @@ describe("replicator probe factories", () => {
|
||||
expect(databaseClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inspects CouchDB identity without encryption setup or remote writes and closes on failure", async () => {
|
||||
const source = { ...createSettings(), encrypt: true };
|
||||
const probe = await createCouchDBConnectionProbeFactory({} as never)(source);
|
||||
const replicator = mocks.couchDB[0];
|
||||
const get = vi.fn(async () => ({ _id: VERSIONING_DOCID, type: "versioninfo", version: 13, used_features: ["nfc-path-identity-v1"] }));
|
||||
const close = vi.fn(async () => undefined);
|
||||
replicator.connectRemoteCouchDBWithSetting.mockResolvedValue({ db: { get }, close });
|
||||
await expect(probe.inspectPathIdentity!()).resolves.toEqual({
|
||||
status: "configured",
|
||||
pathNormalizationVersion: 1,
|
||||
});
|
||||
expect(replicator.connectRemoteCouchDBWithSetting).toHaveBeenCalledWith(
|
||||
{ ...source, encrypt: false },
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
get.mockRejectedValueOnce(new Error("read unavailable"));
|
||||
await expect(probe.inspectPathIdentity!()).rejects.toThrow("read unavailable");
|
||||
expect(close).toHaveBeenCalledTimes(2);
|
||||
expect(replicator.getReplicationPBKDF2Salt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delegates Journal identity inspection to the owned settings snapshot", async () => {
|
||||
const source = createSettings();
|
||||
const snapshot = { ...source };
|
||||
const probe = await createObjectStorageConnectionProbeFactory({} as never)(source);
|
||||
const replicator = mocks.objectStorage[0];
|
||||
replicator.inspectRemotePathIdentity.mockResolvedValue({ status: "empty" });
|
||||
source.bucket = "changed";
|
||||
await expect(probe.inspectPathIdentity!()).resolves.toEqual({ status: "empty" });
|
||||
expect(replicator.inspectRemotePathIdentity).toHaveBeenCalledWith(snapshot);
|
||||
expect(replicator.tryConnectRemote).not.toHaveBeenCalled();
|
||||
expect(replicator.getReplicationPBKDF2Salt).not.toHaveBeenCalled();
|
||||
await probe.dispose();
|
||||
expect(replicator.closeReplication).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("maps a CouchDB connection error string and delegates status to the same snapshot", async () => {
|
||||
const source = createSettings();
|
||||
const snapshot = { ...source };
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
LiveSyncCouchDBReplicator,
|
||||
type LiveSyncCouchDBReplicatorEnv,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { inspectDatabasePathIdentity } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
|
||||
import { createReplicatorDisposer, snapshotRemoteSettings } from "./shared";
|
||||
@@ -51,6 +52,20 @@ function createCouchDBConnectionProbe(
|
||||
await connection.close();
|
||||
}
|
||||
},
|
||||
inspectPathIdentity: async () => {
|
||||
const connection = await replicator.connectRemoteCouchDBWithSetting(
|
||||
{ ...snapshot, encrypt: false },
|
||||
replicator.isMobile(),
|
||||
false,
|
||||
false
|
||||
);
|
||||
if (typeof connection === "string") throw new Error(connection);
|
||||
try {
|
||||
return await inspectDatabasePathIdentity(connection.db);
|
||||
} finally {
|
||||
await connection.close();
|
||||
}
|
||||
},
|
||||
getStatus: () => replicator.getRemoteStatus(snapshot),
|
||||
dispose,
|
||||
};
|
||||
@@ -70,6 +85,7 @@ function createObjectStorageConnectionProbe(
|
||||
return { ok: false, reason: error };
|
||||
}
|
||||
},
|
||||
inspectPathIdentity: () => replicator.inspectRemotePathIdentity(snapshot),
|
||||
getStatus: () => replicator.getRemoteStatus(snapshot),
|
||||
dispose,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user