Fix synchronisation setting compatibility recovery

This commit is contained in:
vorotamoroz committed 2026-09-08 17:16:01 +00:00
1 parent a5056ab157
commit 78c2ccc15a
15 files changed
+1073 -237

No files matched your search

@@ -1,4 +1,5 @@
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { assessTweakCompatibility } from "@vrtmrz/livesync-commonlib/settings";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { balanceChunkPurgedDBs, purgeUnreferencedChunks } from "@vrtmrz/livesync-commonlib/compat/pouchdb/chunks";
@@ -15,7 +16,7 @@ import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
type CentralCompatibilityRecoveryServices = Pick<
LiveSyncBaseCore["services"],
"API" | "appLifecycle" | "replicator" | "tweakValue"
"API" | "appLifecycle" | "replicator" | "setting" | "tweakValue"
>;
/** Collaborators for applying a compatibility decision to its failed publication. */
@@ -145,6 +146,15 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
recovery.reason === CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH &&
recovery.preferredTweakValue
) {
const isCurrent = await context.services.replicator.runWithActiveReplicatorContext(
(activeContext) => activeContext === failedContext
);
// Compare in memory only: these snapshots can contain connection credentials.
if (!isCurrent || JSON.stringify(setting) !== JSON.stringify(context.services.setting.currentSettings())) {
return false;
}
const assessment =
recovery.tweakAssessment ?? assessTweakCompatibility(setting, recovery.preferredTweakValue);
await context.services.tweakValue.askResolvingMismatched(
recovery.preferredTweakValue,
async (effectiveSetting) => {
@@ -156,7 +166,8 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
updated = true;
});
return updated;
}
},
assessment
);
return false;
}
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { assessTweakCompatibility } from "@vrtmrz/livesync-commonlib/settings";
import { defaultLogger, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
import {
CENTRAL_COMPATIBILITY_REJECTION_REASONS,
@@ -23,6 +24,72 @@ import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/rep
import { createCentralCompatibilityRecovery } from "./centralCompatibilityRecovery";
describe("central compatibility recovery", () => {
it("passes the failed attempt's exact tweak assessment to mismatch resolution", async () => {
const setting = { customChunkSize: 0 };
const preferredTweakValue = { customChunkSize: 60 };
const tweakAssessment = assessTweakCompatibility(setting, preferredTweakValue);
const failedContext = { provider: {}, replicator: {} };
const askResolvingMismatched = vi.fn(async (..._args: unknown[]) => "CHECKAGAIN");
const recovery = createCentralCompatibilityRecovery({
services: {
setting: { currentSettings: () => setting },
replicator: {
runWithActiveReplicatorContext: async (task: (context: unknown) => unknown) => task(failedContext),
},
tweakValue: { askResolvingMismatched },
},
} as never);
const result = await recovery.handleReplicationFailure({
context: failedContext,
setting,
outcome: replicationFailed(new Error("mismatched"), {
reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH,
preferredTweakValue,
tweakAssessment,
}),
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
} as never);
expect(askResolvingMismatched.mock.calls[0][2]).toBe(tweakAssessment);
expect(result).toBe(false);
});
it.each(["settings", "publication"])(
"discards a mismatch after its %s changed before recovery",
async (changed) => {
const setting = { customChunkSize: 0, couchDB_DBNAME: "original" };
const failedContext = { provider: {}, replicator: {} };
const currentContext = changed === "publication" ? { provider: {}, replicator: {} } : failedContext;
const currentSetting = changed === "settings" ? { ...setting, couchDB_DBNAME: "replacement" } : setting;
const askResolvingMismatched = vi.fn(async () => "CHECKAGAIN");
const recovery = createCentralCompatibilityRecovery({
services: {
setting: { currentSettings: () => currentSetting },
replicator: {
runWithActiveReplicatorContext: async (task: (context: unknown) => unknown) =>
task(currentContext),
},
tweakValue: { askResolvingMismatched },
},
} as never);
await recovery.handleReplicationFailure({
context: failedContext,
setting,
outcome: replicationFailed(new Error("mismatched"), {
reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH,
preferredTweakValue: { customChunkSize: 60 },
}),
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
} as never);
expect(askResolvingMismatched).not.toHaveBeenCalled();
}
);
it("characterises unattended central failure handling as one INFO log without a NOTICE", async () => {
const log = vi.fn((_message: unknown, _level?: number, _key?: string) => undefined);
setGlobalLogFunction(log);
@@ -69,6 +136,7 @@ describe("central compatibility recovery", () => {
};
const failedContext = { provider: {}, replicator: failedReplicator };
const replacementContext = { provider: {}, replicator: replacementReplicator };
let activeContext = failedContext;
const preferredTweakValue = { customChunkSize: 60 };
const outcome = replicationFailed(new Error("mismatched"), {
reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH,
@@ -81,9 +149,10 @@ describe("central compatibility recovery", () => {
services: {
appLifecycle: {},
API: {},
setting: { currentSettings: () => ({}) },
replicator: {
runWithActiveReplicatorContext: vi.fn(async (task: (context: unknown) => unknown) =>
task(replacementContext)
task(activeContext)
),
},
tweakValue: { askResolvingMismatched },
@@ -118,10 +187,15 @@ describe("central compatibility recovery", () => {
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
} as never);
expect(askResolvingMismatched).toHaveBeenCalledWith(preferredTweakValue, expect.any(Function));
expect(askResolvingMismatched).toHaveBeenCalledWith(
preferredTweakValue,
expect.any(Function),
assessTweakCompatibility({}, preferredTweakValue)
);
const updatePreferredRemote = askResolvingMismatched.mock.calls[0][1] as (
setting: Record<string, unknown>
) => Promise<boolean>;
activeContext = replacementContext;
await expect(updatePreferredRemote({ customChunkSize: 64 })).resolves.toBe(false);
expect(failedSetPreferred).not.toHaveBeenCalled();
expect(replacementSetPreferred).not.toHaveBeenCalled();
@@ -143,6 +217,7 @@ describe("central compatibility recovery", () => {
services: {
appLifecycle: {},
API: {},
setting: { currentSettings: () => ({}) },
replicator: {
runWithActiveReplicatorContext: vi.fn(async (task: (context: unknown) => unknown) =>
task(failedContext)
+1
View File
@@ -86,6 +86,7 @@ export function useReplicationFeature<TContext extends ServiceContext, TCommands
API: services.API,
appLifecycle: services.appLifecycle,
replicator: services.replicator,
setting: services.setting,
tweakValue: services.tweakValue,
},
});