mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-03 14:58:18 +00:00
133 lines
5.9 KiB
TypeScript
133 lines
5.9 KiB
TypeScript
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 {
|
|
CENTRAL_COMPATIBILITY_REJECTION_REASONS,
|
|
type ReplicatorInstance,
|
|
type ReplicationFailureRequest,
|
|
} from "@vrtmrz/livesync-commonlib/replication";
|
|
import { $msg } from "@/common/translation";
|
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
|
|
|
type CentralCompatibilityRecoveryServices = Pick<
|
|
LiveSyncBaseCore["services"],
|
|
"appLifecycle" | "replicator" | "setting" | "tweakValue"
|
|
>;
|
|
|
|
/** Collaborators for applying a compatibility decision to its failed publication. */
|
|
interface CentralCompatibilityRecoveryContext {
|
|
readonly confirm: LiveSyncBaseCore["confirm"];
|
|
readonly rebuilder: LiveSyncBaseCore["rebuilder"];
|
|
readonly services: CentralCompatibilityRecoveryServices;
|
|
}
|
|
|
|
interface PreferredRemoteTweakWriter extends ReplicatorInstance {
|
|
setPreferredRemoteTweakSettings(setting: ObsidianLiveSyncSettings): Promise<void>;
|
|
}
|
|
|
|
interface ResolvedRemoteWriter extends ReplicatorInstance {
|
|
markRemoteResolved(setting: ObsidianLiveSyncSettings): Promise<void>;
|
|
}
|
|
|
|
function canSetPreferredRemoteTweakSettings(replicator: ReplicatorInstance): replicator is PreferredRemoteTweakWriter {
|
|
return (
|
|
"setPreferredRemoteTweakSettings" in replicator &&
|
|
typeof replicator.setPreferredRemoteTweakSettings === "function"
|
|
);
|
|
}
|
|
|
|
function canMarkRemoteResolved(replicator: ReplicatorInstance): replicator is ResolvedRemoteWriter {
|
|
return "markRemoteResolved" in replicator && typeof replicator.markRemoteResolved === "function";
|
|
}
|
|
|
|
/**
|
|
* Compose central compatibility recovery around the exact failed publication.
|
|
* Remote mutations re-admit that publication and become no-ops after a
|
|
* replacement; the failure result is never re-read from the current instance.
|
|
*/
|
|
export function createCentralCompatibilityRecovery(context: CentralCompatibilityRecoveryContext) {
|
|
async function handleReplicationFailure(request: ReplicationFailureRequest): Promise<boolean> {
|
|
const { context: failedContext, interaction, outcome, setting } = request;
|
|
if (interaction.kind === "forbidden") {
|
|
// Automatic requests may report the failure, but must not enter
|
|
// tweak, lock, fetch, or unlock dialogues.
|
|
Logger("Replication failed on an unattended path.", LOG_LEVEL_INFO);
|
|
return false;
|
|
}
|
|
if (!interaction.permissions.failureRecovery) return false;
|
|
const recovery = outcome.recoveryHint;
|
|
if (!recovery) return false;
|
|
|
|
if (
|
|
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) => {
|
|
let updated = false;
|
|
await context.services.replicator.runWithActiveReplicatorContext(async (activeContext) => {
|
|
if (activeContext !== failedContext) return;
|
|
if (!canSetPreferredRemoteTweakSettings(activeContext.replicator)) return;
|
|
await activeContext.replicator.setPreferredRemoteTweakSettings({ ...effectiveSetting });
|
|
updated = true;
|
|
});
|
|
return updated;
|
|
},
|
|
assessment
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
recovery.reason !== CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_LOCKED &&
|
|
recovery.reason !== CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_CLEANED
|
|
) {
|
|
return false;
|
|
}
|
|
const message = $msg("Replicator.Dialogue.Locked.Message");
|
|
const CHOICE_FETCH = $msg("Replicator.Dialogue.Locked.Action.Fetch");
|
|
const CHOICE_DISMISS = $msg("Replicator.Dialogue.Locked.Action.Dismiss");
|
|
const CHOICE_UNLOCK = $msg("Replicator.Dialogue.Locked.Action.Unlock");
|
|
const selected = await context.confirm.askSelectStringDialogue(
|
|
message,
|
|
[CHOICE_FETCH, CHOICE_UNLOCK, CHOICE_DISMISS],
|
|
{
|
|
title: $msg("Replicator.Dialogue.Locked.Title"),
|
|
defaultAction: CHOICE_DISMISS,
|
|
timeout: 60,
|
|
}
|
|
);
|
|
if (selected == CHOICE_FETCH) {
|
|
Logger($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE);
|
|
await context.rebuilder.scheduleFetch();
|
|
context.services.appLifecycle.scheduleRestart();
|
|
return false;
|
|
}
|
|
if (selected != CHOICE_UNLOCK) return false;
|
|
|
|
let unlocked = false;
|
|
await context.services.replicator.runWithActiveReplicatorContext(async (activeContext) => {
|
|
if (activeContext !== failedContext) return;
|
|
if (!canMarkRemoteResolved(activeContext.replicator)) return;
|
|
await activeContext.replicator.markRemoteResolved(setting);
|
|
unlocked = true;
|
|
});
|
|
if (unlocked) {
|
|
Logger($msg("Replicator.Dialogue.Locked.Message.Unlocked"), LOG_LEVEL_NOTICE);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return Object.freeze({ handleReplicationFailure });
|
|
}
|