Retire legacy purge-based cleanup recovery

This commit is contained in:
vorotamoroz committed 2026-10-31 09:44:02 +00:00
1 parent f325d2454d
commit d1af8a5dfa
4 files changed
+129 -223

No files matched your search

@@ -1,48 +1,22 @@
import {
VERSIONING_DOCID,
type EntryDoc,
type ObsidianLiveSyncSettings,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
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";
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
import {
CENTRAL_COMPATIBILITY_REJECTION_REASONS,
REPLICATION_PROGRESS_PRESENTATIONS,
assessRemoteFeatureDocument,
describeRemoteFeatureRejection,
type ReplicatorInstance,
type ReplicationFailureRequest,
} from "@vrtmrz/livesync-commonlib/replication";
import { $msg } from "@/common/translation";
import { usesLegacyIndexedDBAdapter } from "@/common/compatibilitySettings";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import type PouchDB from "pouchdb-core";
async function canInterpretCleanupDatabase(db: PouchDB.Database<EntryDoc>): Promise<boolean> {
try {
const assessment = assessRemoteFeatureDocument(await db.get(VERSIONING_DOCID));
if (assessment.status === "supported" || assessment.status === "older-generation") return true;
Logger(`Database cleanup cancelled: ${describeRemoteFeatureRejection(assessment)}`, LOG_LEVEL_NOTICE);
} catch (error) {
Logger("Database cleanup cancelled: feature compatibility could not be checked.", LOG_LEVEL_NOTICE);
Logger(error, LOG_LEVEL_INFO);
}
return false;
}
type CentralCompatibilityRecoveryServices = Pick<
LiveSyncBaseCore["services"],
"API" | "appLifecycle" | "replicator" | "setting" | "tweakValue"
"appLifecycle" | "replicator" | "setting" | "tweakValue"
>;
/** Collaborators for applying a compatibility decision to its failed publication. */
interface CentralCompatibilityRecoveryContext {
readonly confirm: LiveSyncBaseCore["confirm"];
/** Obtain the database only when recovery runs, after initialisation or reset. */
readonly getLocalDatabase: () => LiveSyncBaseCore["localDatabase"];
readonly rebuilder: LiveSyncBaseCore["rebuilder"];
readonly services: CentralCompatibilityRecoveryServices;
}
@@ -72,89 +46,11 @@ function canMarkRemoteResolved(replicator: ReplicatorInstance): replicator is Re
* replacement; the failure result is never re-read from the current instance.
*/
export function createCentralCompatibilityRecovery(context: CentralCompatibilityRecoveryContext) {
async function reconcileCleanedRemote(
showProgress: boolean,
setting: ObsidianLiveSyncSettings,
expectedContext: ReplicationFailureRequest["context"]
) {
Logger("The remote database has been cleaned.", showProgress ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
await skipIfDuplicated("cleanup", async () => {
if (!(await canInterpretCleanupDatabase(context.getLocalDatabase().localDatabase))) return;
const count = await purgeUnreferencedChunks(context.getLocalDatabase().localDatabase, true);
const message = `The remote database has been cleaned up.
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
However, If there are many chunks to be deleted, maybe fetching again is faster.
We will lose the history of this device if we fetch the remote database again.
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`;
const CHOICE_FETCH = "Fetch again";
const CHOICE_CLEAN = "Cleanup";
const CHOICE_DISMISS = "Dismiss";
const selected = await context.confirm.confirmWithMessage(
"Cleaned",
message,
[CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS],
CHOICE_DISMISS,
30
);
if (selected == CHOICE_FETCH) {
await context.rebuilder.$performRebuildDB("localOnly");
}
if (selected != CHOICE_CLEAN) return;
await context.services.replicator.runBoundedRemoteActivity(
() =>
context.services.replicator.runWithActiveReplicatorContext(async (activeContext) => {
if (activeContext !== expectedContext) return;
const replicator = activeContext.replicator;
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
const localDatabase = context.getLocalDatabase();
const remoteDatabase = await replicator.connectRemoteCouchDBWithSetting(
setting,
context.services.API.isMobile(),
true
);
if (typeof remoteDatabase == "string") {
Logger(remoteDatabase, LOG_LEVEL_NOTICE);
return false;
}
try {
await purgeUnreferencedChunks(localDatabase.localDatabase, false);
localDatabase.clearCaches();
const replicated = await context.services.replicator.runFiniteReplicationActivity(
() => replicator.openOneShotReplication(setting, showProgress, false, "sync", true),
{ label: "replication" }
);
if (replicated) {
await balanceChunkPurgedDBs(localDatabase.localDatabase, remoteDatabase.db);
await purgeUnreferencedChunks(localDatabase.localDatabase, false);
localDatabase.clearCaches();
await replicator.markRemoteResolved(setting);
Logger(
"The local database has been cleaned up.",
showProgress ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
} else {
Logger(
"Replication has been cancelled. Please try it again.",
showProgress ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
}
} finally {
await remoteDatabase.close();
}
}),
{ label: "database-cleanup" }
);
});
}
async function handleReplicationFailure(request: ReplicationFailureRequest): Promise<boolean> {
const { context: failedContext, interaction, outcome, progressPresentation, setting } = request;
const showProgress = progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE;
const { context: failedContext, interaction, outcome, setting } = request;
if (interaction.kind === "forbidden") {
// Automatic requests may report the failure, but must not enter
// tweak, lock, fetch, unlock, or cleanup dialogues.
// tweak, lock, fetch, or unlock dialogues.
Logger("Replication failed on an unattended path.", LOG_LEVEL_INFO);
return false;
}
@@ -198,14 +94,6 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
) {
return false;
}
if (
recovery.reason === CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_CLEANED &&
usesLegacyIndexedDBAdapter(setting)
) {
await reconcileCleanedRemote(showProgress, setting, failedContext);
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");
@@ -240,5 +128,5 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
return false;
}
return Object.freeze({ handleReplicationFailure, reconcileCleanedRemote });
return Object.freeze({ handleReplicationFailure });
}