Complete central provider, resource, scheduling, and recovery contracts

This commit is contained in:
vorotamoroz committed 2026-08-31 15:06:06 +00:00
1 parent 24228bf7cf
commit 1d2077d3fc
18 files changed
+464 -34

No files matched your search

@@ -21,7 +21,23 @@ import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/ser
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";
import {
CENTRAL_COMPATIBILITY_REJECTION_REASONS,
REMOTE_RESOURCE_KINDS,
type ReplicationAttemptFailure,
type ReplicatorInstance,
} from "@vrtmrz/livesync-commonlib/replication";
interface PreferredRemoteTweakWriter extends ReplicatorInstance {
setPreferredRemoteTweakSettings(setting: ObsidianLiveSyncSettings): Promise<void>;
}
function canSetPreferredRemoteTweakSettings(replicator: ReplicatorInstance): replicator is PreferredRemoteTweakWriter {
return (
"setPreferredRemoteTweakSettings" in replicator &&
typeof replicator.setPreferredRemoteTweakSettings === "function"
);
}
/**
* Localised counterpart of Commonlib's `confName()`, which takes no translator.
@@ -114,11 +130,27 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
});
}
async _anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false;
const preferred = this.core.replicator.preferredTweakValue;
if (!preferred) return false;
const ret = await this.services.tweakValue.askResolvingMismatched(preferred);
async _anyAfterConnectCheckFailed(failure: ReplicationAttemptFailure): Promise<boolean | "CHECKAGAIN" | undefined> {
const recovery = failure.outcome.recoveryHint;
if (
recovery?.reason !== CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH ||
!recovery.preferredTweakValue
) {
return false;
}
const ret = await this.services.tweakValue.askResolvingMismatched(
{ ...recovery.preferredTweakValue },
async (setting) => {
let updated = false;
await this.services.replicator.runWithActiveReplicatorContext(async (activeContext) => {
if (activeContext !== failure.context) return;
if (!canSetPreferredRemoteTweakSettings(activeContext.replicator)) return;
await activeContext.replicator.setPreferredRemoteTweakSettings({ ...setting });
updated = true;
});
return updated;
}
);
if (ret == "OK") return false;
if (ret == "CHECKAGAIN") return "CHECKAGAIN";
if (ret == "IGNORE") return true;
@@ -236,8 +268,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
preferredSource: TweakValues,
updatePreferredRemote?: (setting: ObsidianLiveSyncSettings) => Promise<boolean>
): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
const [conf, rebuildRequired] =
await this.services.tweakValue.checkAndAskResolvingMismatched(preferredSource);
const [conf, rebuildRequired] = await this.services.tweakValue.checkAndAskResolvingMismatched(preferredSource);
if (!conf) return "IGNORE";
const updateRemote = async () => {
@@ -7,7 +7,12 @@ 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";
import {
CENTRAL_COMPATIBILITY_REJECTION_REASONS,
REMOTE_RESOURCE_KINDS,
USER_INITIATED_REPLICATION_AUTHORITY,
type ReplicationAttemptFailure,
} from "@vrtmrz/livesync-commonlib/replication";
function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
const askSelectStringDialogue = vi.fn(async (..._args: unknown[]): Promise<string | undefined> => undefined);
@@ -56,6 +61,75 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
}
describe("ModuleResolvingMismatchedTweaks", () => {
it("uses the failed attempt hint and writes only through that exact active publication", async () => {
const { module, core } = createModule();
const attemptPreferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
customChunkSize: 60,
};
const replacementPreferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
customChunkSize: 99,
};
let updatePreferredRemote: ((setting: typeof core.settings) => Promise<boolean>) | undefined;
const askResolvingMismatched = vi.fn(
async (_preferred: unknown, update: (setting: typeof core.settings) => Promise<boolean>) => {
updatePreferredRemote = update;
return "IGNORE" as const;
}
);
core._services.tweakValue = { askResolvingMismatched };
core.replicator = {
tweakSettingsMismatched: true,
preferredTweakValue: replacementPreferred,
};
const failedSetPreferred = vi.fn(async (_setting: typeof core.settings) => undefined);
const replacementSetPreferred = vi.fn(async (_setting: typeof core.settings) => undefined);
const failedContext = {
provider: {},
replicator: { setPreferredRemoteTweakSettings: failedSetPreferred },
configurationIdentity: "profile-a",
};
const replacementContext = {
provider: {},
replicator: { setPreferredRemoteTweakSettings: replacementSetPreferred },
configurationIdentity: "profile-b",
};
let activeContext = failedContext;
core._services.replicator = {
runWithActiveReplicatorContext: vi.fn(async (task: (context: typeof failedContext) => unknown) =>
task(activeContext)
),
};
const request = {
context: failedContext,
setting: core.settings,
outcome: {
status: "failed" as const,
error: new Error("directional replication failed"),
recoveryHint: {
reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH,
preferredTweakValue: attemptPreferred,
},
},
showMessage: true,
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
} as unknown as ReplicationAttemptFailure;
await expect(module._anyAfterConnectCheckFailed(request)).resolves.toBe(true);
expect(askResolvingMismatched).toHaveBeenCalledWith(attemptPreferred, expect.any(Function));
const effectiveSetting = { ...core.settings, customChunkSize: 64 };
await expect(updatePreferredRemote?.(effectiveSetting)).resolves.toBe(true);
expect(failedSetPreferred).toHaveBeenCalledWith(effectiveSetting);
expect(failedSetPreferred.mock.calls[0][0]).not.toBe(effectiveSetting);
activeContext = replacementContext;
await expect(updatePreferredRemote?.({ ...effectiveSetting, customChunkSize: 72 })).resolves.toBe(false);
expect(failedSetPreferred).toHaveBeenCalledOnce();
expect(replacementSetPreferred).not.toHaveBeenCalled();
});
it("returns an unconfigured remote result without a separate connection preflight", async () => {
const { module, core } = createModule();
const read = vi.fn(async () => ({