mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-02 22:38:18 +00:00
Retire legacy purge-based cleanup recovery
This commit is contained in:
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 });
|
||||
}
|
||||
@@ -16,55 +16,86 @@ const chunkMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/chunks", () => chunkMocks);
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||
LiveSyncCouchDBReplicator: class {},
|
||||
}));
|
||||
|
||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { createCentralCompatibilityRecovery } from "./centralCompatibilityRecovery";
|
||||
|
||||
describe("central compatibility recovery", () => {
|
||||
it("does not count chunks for cleanup when local feature requirements are unknown", async () => {
|
||||
chunkMocks.purgeUnreferencedChunks.mockClear();
|
||||
const confirmWithMessage = vi.fn(async () => "Dismiss");
|
||||
const recovery = createCentralCompatibilityRecovery({
|
||||
confirm: { confirmWithMessage },
|
||||
getLocalDatabase: () => ({
|
||||
localDatabase: {
|
||||
get: vi.fn(async (id: string) => ({
|
||||
_id: id,
|
||||
type: "versioninfo",
|
||||
version: 13,
|
||||
used_features: ["future-format-v7"],
|
||||
})),
|
||||
},
|
||||
}),
|
||||
services: { replicator: {} },
|
||||
} as never);
|
||||
describe.each([
|
||||
{ name: "unknown local features", version: 13, used_features: ["future-format-v7"] },
|
||||
{ name: "a legacy local version", version: 11 },
|
||||
{ name: "the current local version", version: 12 },
|
||||
])("compatibility: cleaned IndexedDB remote with $name", (localVersion) => {
|
||||
it.each(["fetch", "unlock", "dismiss"] as const)(
|
||||
"uses standard recovery for an explicit %s choice without legacy cleanup",
|
||||
async (choice) => {
|
||||
chunkMocks.purgeUnreferencedChunks.mockClear();
|
||||
chunkMocks.balanceChunkPurgedDBs.mockClear();
|
||||
const markRemoteResolved = vi.fn(async (_setting: unknown) => undefined);
|
||||
const failedContext = {
|
||||
provider: {},
|
||||
replicator: { markRemoteResolved },
|
||||
configurationIdentity: "cleaned-remote-test",
|
||||
};
|
||||
const setting = { useIndexedDBAdapter: true } as ObsidianLiveSyncSettings;
|
||||
const scheduleFetch = vi.fn(async () => undefined);
|
||||
const scheduleRestart = vi.fn();
|
||||
const getLocalDatabase = vi.fn(() => ({
|
||||
localDatabase: {
|
||||
get: vi.fn(async () => ({ _id: VERSIONING_DOCID, type: "versioninfo", ...localVersion })),
|
||||
},
|
||||
}));
|
||||
const confirmWithMessage = vi.fn(async () => "Dismiss");
|
||||
const askSelectStringDialogue = vi.fn(
|
||||
async (_message: string, choices: string[]) =>
|
||||
choices[choice === "fetch" ? 0 : choice === "unlock" ? 1 : 2]
|
||||
);
|
||||
const recovery = createCentralCompatibilityRecovery({
|
||||
confirm: { askSelectStringDialogue, confirmWithMessage },
|
||||
getLocalDatabase,
|
||||
rebuilder: { scheduleFetch },
|
||||
services: {
|
||||
appLifecycle: { scheduleRestart },
|
||||
replicator: {
|
||||
runWithActiveReplicatorContext: async (task: (context: unknown) => unknown) =>
|
||||
await task(failedContext),
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
await recovery.reconcileCleanedRemote(true, {} as ObsidianLiveSyncSettings, {} as never);
|
||||
const result = await recovery.handleReplicationFailure({
|
||||
context: failedContext,
|
||||
setting,
|
||||
outcome: replicationFailed(new Error("cleaned"), {
|
||||
reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_CLEANED,
|
||||
}),
|
||||
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
|
||||
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
|
||||
} as never);
|
||||
|
||||
expect(chunkMocks.purgeUnreferencedChunks).not.toHaveBeenCalled();
|
||||
expect(confirmWithMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows cleanup counting for a legacy local version document", async () => {
|
||||
chunkMocks.purgeUnreferencedChunks.mockClear();
|
||||
const confirmWithMessage = vi.fn(async () => "Dismiss");
|
||||
const recovery = createCentralCompatibilityRecovery({
|
||||
confirm: { confirmWithMessage },
|
||||
getLocalDatabase: () => ({
|
||||
localDatabase: {
|
||||
get: vi.fn(async (id: string) => ({ _id: id, type: "versioninfo", version: 11 })),
|
||||
},
|
||||
}),
|
||||
services: { replicator: {} },
|
||||
} as never);
|
||||
|
||||
await recovery.reconcileCleanedRemote(true, {} as ObsidianLiveSyncSettings, {} as never);
|
||||
|
||||
expect(chunkMocks.purgeUnreferencedChunks).toHaveBeenCalledWith(expect.anything(), true);
|
||||
expect(confirmWithMessage).toHaveBeenCalledOnce();
|
||||
expect(result).toBe(false);
|
||||
expect(askSelectStringDialogue).toHaveBeenCalledOnce();
|
||||
const choices = askSelectStringDialogue.mock.calls[0][1];
|
||||
expect(choices).toHaveLength(3);
|
||||
expect(askSelectStringDialogue).toHaveBeenCalledWith(expect.any(String), choices, {
|
||||
title: expect.any(String),
|
||||
defaultAction: choices[2],
|
||||
timeout: 60,
|
||||
});
|
||||
expect(confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(getLocalDatabase).not.toHaveBeenCalled();
|
||||
expect(chunkMocks.purgeUnreferencedChunks).not.toHaveBeenCalled();
|
||||
expect(chunkMocks.balanceChunkPurgedDBs).not.toHaveBeenCalled();
|
||||
expect(scheduleFetch).toHaveBeenCalledTimes(choice === "fetch" ? 1 : 0);
|
||||
expect(scheduleRestart).toHaveBeenCalledTimes(choice === "fetch" ? 1 : 0);
|
||||
expect(markRemoteResolved).toHaveBeenCalledTimes(choice === "unlock" ? 1 : 0);
|
||||
if (choice === "fetch") {
|
||||
expect(scheduleFetch.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
scheduleRestart.mock.invocationCallOrder[0]
|
||||
);
|
||||
}
|
||||
if (choice === "unlock") expect(markRemoteResolved).toHaveBeenCalledWith(setting);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("passes the failed attempt's exact tweak assessment to mismatch resolution", async () => {
|
||||
@@ -139,7 +170,6 @@ describe("central compatibility recovery", () => {
|
||||
try {
|
||||
const recovery = createCentralCompatibilityRecovery({
|
||||
confirm: {},
|
||||
getLocalDatabase: () => ({}),
|
||||
rebuilder: {},
|
||||
services: {
|
||||
appLifecycle: {},
|
||||
@@ -187,7 +217,6 @@ describe("central compatibility recovery", () => {
|
||||
});
|
||||
const recovery = createCentralCompatibilityRecovery({
|
||||
confirm: {},
|
||||
getLocalDatabase: () => ({}),
|
||||
rebuilder: {},
|
||||
services: {
|
||||
appLifecycle: {},
|
||||
@@ -255,7 +284,6 @@ describe("central compatibility recovery", () => {
|
||||
);
|
||||
const recovery = createCentralCompatibilityRecovery({
|
||||
confirm: {},
|
||||
getLocalDatabase: () => ({}),
|
||||
rebuilder: {},
|
||||
services: {
|
||||
appLifecycle: {},
|
||||
@@ -287,7 +315,10 @@ describe("central compatibility recovery", () => {
|
||||
expect(setPreferredRemoteTweakSettings.mock.calls[0][0]).not.toBe(effectiveSetting);
|
||||
});
|
||||
|
||||
it("does not apply an unlock selected for a replaced failed publication", async () => {
|
||||
it.each([
|
||||
CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_LOCKED,
|
||||
CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_CLEANED,
|
||||
])("does not apply an unlock for a replaced failed publication: %s", async (reason) => {
|
||||
const failedMarkResolved = vi.fn(async () => undefined);
|
||||
const replacementMarkResolved = vi.fn(async () => undefined);
|
||||
const failedContext = { provider: {}, replicator: { markRemoteResolved: failedMarkResolved } };
|
||||
@@ -299,7 +330,6 @@ describe("central compatibility recovery", () => {
|
||||
confirm: {
|
||||
askSelectStringDialogue: vi.fn(async (_message: string, choices: string[]) => choices[1]),
|
||||
},
|
||||
getLocalDatabase: () => ({}),
|
||||
rebuilder: {},
|
||||
services: {
|
||||
appLifecycle: { scheduleRestart: vi.fn() },
|
||||
@@ -311,9 +341,9 @@ describe("central compatibility recovery", () => {
|
||||
|
||||
await recovery.handleReplicationFailure({
|
||||
context: failedContext,
|
||||
setting: {},
|
||||
setting: { useIndexedDBAdapter: true },
|
||||
outcome: replicationFailed(new Error("locked"), {
|
||||
reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_LOCKED,
|
||||
reason,
|
||||
}),
|
||||
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
|
||||
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
|
||||
@@ -324,69 +354,55 @@ describe("central compatibility recovery", () => {
|
||||
expect(replacementMarkResolved).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps cleaned-remote replication and balancing inside the shared activity boundary", async () => {
|
||||
const activityFinished = vi.fn();
|
||||
const runBoundedRemoteActivity = vi.fn(async (task: () => unknown) => {
|
||||
try {
|
||||
return await task();
|
||||
} finally {
|
||||
activityFinished();
|
||||
}
|
||||
});
|
||||
const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task());
|
||||
const openOneShotReplication = vi.fn(async () => true);
|
||||
const remoteDatabase = {
|
||||
close: vi.fn(async () => undefined),
|
||||
it.each([
|
||||
NO_INTERACTION,
|
||||
{
|
||||
kind: "permitted" as const,
|
||||
permissions: { ...USER_INITIATED_REPLICATION_AUTHORITY.permissions, failureRecovery: false },
|
||||
},
|
||||
])("does not recover a cleaned remote without recovery authority: %j", async (interaction) => {
|
||||
const askSelectStringDialogue = vi.fn();
|
||||
const confirmWithMessage = vi.fn();
|
||||
const getLocalDatabase = vi.fn();
|
||||
const scheduleFetch = vi.fn();
|
||||
const scheduleRestart = vi.fn();
|
||||
const markRemoteResolved = vi.fn();
|
||||
const failedContext = {
|
||||
provider: {},
|
||||
replicator: { markRemoteResolved },
|
||||
configurationIdentity: "cleaned-remote-test",
|
||||
};
|
||||
const close = vi.fn(async () => undefined);
|
||||
const activeReplicator = Object.assign(new LiveSyncCouchDBReplicator({} as never), {
|
||||
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase, close })),
|
||||
openOneShotReplication,
|
||||
markRemoteResolved: vi.fn(async () => undefined),
|
||||
});
|
||||
const expectedContext = { provider: {}, replicator: activeReplicator };
|
||||
const runWithActiveReplicatorContext = vi.fn(async (task: (context: unknown) => unknown) =>
|
||||
task(expectedContext)
|
||||
task(failedContext)
|
||||
);
|
||||
const localDatabase = {
|
||||
localDatabase: {
|
||||
get: vi.fn(async () => ({ _id: VERSIONING_DOCID, type: "versioninfo", version: 12 })),
|
||||
},
|
||||
clearCaches: vi.fn(),
|
||||
};
|
||||
const getLocalDatabase = vi.fn(() => localDatabase);
|
||||
const recovery = createCentralCompatibilityRecovery({
|
||||
confirm: { confirmWithMessage: vi.fn(async () => "Cleanup") },
|
||||
confirm: { askSelectStringDialogue, confirmWithMessage },
|
||||
getLocalDatabase,
|
||||
rebuilder: {},
|
||||
rebuilder: { scheduleFetch },
|
||||
services: {
|
||||
appLifecycle: {},
|
||||
API: { isMobile: vi.fn(() => false) },
|
||||
replicator: {
|
||||
runBoundedRemoteActivity,
|
||||
runFiniteReplicationActivity,
|
||||
runWithActiveReplicatorContext,
|
||||
},
|
||||
tweakValue: {},
|
||||
appLifecycle: { scheduleRestart },
|
||||
replicator: { runWithActiveReplicatorContext },
|
||||
},
|
||||
} as never);
|
||||
|
||||
await recovery.reconcileCleanedRemote(true, {} as ObsidianLiveSyncSettings, expectedContext as never);
|
||||
await expect(
|
||||
recovery.handleReplicationFailure({
|
||||
context: failedContext,
|
||||
setting: { useIndexedDBAdapter: true },
|
||||
outcome: replicationFailed(new Error("cleaned"), {
|
||||
reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_CLEANED,
|
||||
}),
|
||||
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
|
||||
interaction,
|
||||
} as never)
|
||||
).resolves.toBe(false);
|
||||
|
||||
expect(runBoundedRemoteActivity).toHaveBeenCalledWith(expect.any(Function), {
|
||||
label: "database-cleanup",
|
||||
});
|
||||
expect(runFiniteReplicationActivity).toHaveBeenCalledWith(expect.any(Function), {
|
||||
label: "replication",
|
||||
});
|
||||
expect(runWithActiveReplicatorContext).toHaveBeenCalledOnce();
|
||||
expect(openOneShotReplication).toHaveBeenCalledOnce();
|
||||
expect(openOneShotReplication.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
activityFinished.mock.invocationCallOrder[0]
|
||||
);
|
||||
expect(chunkMocks.balanceChunkPurgedDBs).toHaveBeenCalledOnce();
|
||||
expect(getLocalDatabase).toHaveBeenCalled();
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
expect(close.mock.invocationCallOrder[0]).toBeLessThan(activityFinished.mock.invocationCallOrder[0]);
|
||||
expect(askSelectStringDialogue).not.toHaveBeenCalled();
|
||||
expect(confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(getLocalDatabase).not.toHaveBeenCalled();
|
||||
expect(scheduleFetch).not.toHaveBeenCalled();
|
||||
expect(scheduleRestart).not.toHaveBeenCalled();
|
||||
expect(runWithActiveReplicatorContext).not.toHaveBeenCalled();
|
||||
expect(markRemoteResolved).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -83,10 +83,8 @@ export function useReplicationFeature<TContext extends ServiceContext, TCommands
|
||||
const securitySeedPreflight = createSecuritySeedPreflight(unresolvedErrorManager, preflightContext);
|
||||
const centralCompatibilityRecovery = createCentralCompatibilityRecovery({
|
||||
confirm: core.confirm,
|
||||
getLocalDatabase: () => core.localDatabase,
|
||||
rebuilder: core.rebuilder,
|
||||
services: {
|
||||
API: services.API,
|
||||
appLifecycle: services.appLifecycle,
|
||||
replicator: services.replicator,
|
||||
setting: services.setting,
|
||||
|
||||
@@ -29,6 +29,10 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
||||
|
||||
### Synchronisation
|
||||
|
||||
- Devices using the legacy IndexedDB adapter now use the standard recovery dialogue when a remote retains an old cleanup lock.
|
||||
- The obsolete in-place `Cleanup` option has been removed. We can fetch the remote again, explicitly unlock the device, or dismiss the dialogue; dismissal remains the default.
|
||||
- Fetch replaces the local database, so preserve unsynchronised work and any required local history first. Unlock only when the local and remote databases are already aligned. Garbage Collection V3 is unchanged.
|
||||
|
||||
- Added an optional, device-local editing guard for substantial CouchDB LiveSync catch-up transfers. When enough incoming notes are detected, it covers the workspace until the transfer and local application queues have settled. The overlay can always be bypassed, and ordinary small transfers remain uninterrupted.
|
||||
|
||||
- We can see CouchDB change-feed comparison progress separately for push and pull.
|
||||
|
||||
Reference in new issue
Block a user