mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-28 13:32:31 +00:00
Merge pull request #1208 from kimjansheden/fix/remediation-result-application
Apply received changes again while remediation mode is active
This commit is contained in:
@@ -190,6 +190,20 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
|
||||
log: LogFunction,
|
||||
cleanupFlag: () => Promise<void>
|
||||
): Promise<boolean | undefined> {
|
||||
if (host.services.setting.currentSettings().maxMTimeForReflectEvents > 0) {
|
||||
// Simple Fetch reconciles storage with the local database after fetching, past the check which
|
||||
// refuses that scan in remediation mode. Skipping only the scan would restore nothing from most
|
||||
// remotes: reflection of received documents stays suspended while Simple Fetch fetches, so Object
|
||||
// Storage and P2P remotes discard them, and CouchDB Fast Fetch writes them straight into the
|
||||
// database. The detailed flow at least states the restriction and offers to clear it before
|
||||
// rebuilding, instead of quietly reconciling past it.
|
||||
log(
|
||||
"Remediation mode is active, so the detailed fetch flow is used instead of Simple Fetch.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
clearRememberedSimpleFetchMode(host);
|
||||
return undefined;
|
||||
}
|
||||
const result = await askSimpleFetchMode(host);
|
||||
if (result === "cancelled") {
|
||||
log("Fetch cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
|
||||
@@ -771,6 +771,24 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
describe("askAndPerformFastSetupOnScheduledFetchAll", () => {
|
||||
it("uses the detailed flow instead of Simple Fetch while remediation mode is active", async () => {
|
||||
const host = createHostMock();
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
Object.assign(host.mocks.setting.settings, {
|
||||
maxMTimeForReflectEvents: Date.parse("2026-09-01T00:00:00Z"),
|
||||
});
|
||||
|
||||
await expect(askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag)).resolves.toBe(
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(host.mocks.setting.deleteSmallConfig).toHaveBeenCalledWith("simple-fetch-mode");
|
||||
expect(cleanupFlag).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("releases both reflection suspensions after Fast Setup succeeds", async () => {
|
||||
const host = createHostMock();
|
||||
const log = createLoggerMock();
|
||||
|
||||
@@ -35,7 +35,7 @@ type ReplicateResultProcessorSettings = Pick<
|
||||
>;
|
||||
type ReplicateResultProcessorServices = Pick<
|
||||
LiveSyncBaseCore["services"],
|
||||
"appLifecycle" | "path" | "replication" | "vault"
|
||||
"appLifecycle" | "database" | "path" | "replication" | "vault"
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -115,10 +115,27 @@ export class ReplicateResultProcessor {
|
||||
// If true, the processing queue processor bails the loop.
|
||||
private _suspended: boolean = false;
|
||||
|
||||
/**
|
||||
* Whether the application accepts replicated documents being applied.
|
||||
*
|
||||
* Remediation mode refuses the reconciliation scan which readiness depends upon, so the
|
||||
* application stays unready for as long as the modification-time limit is configured.
|
||||
* Applying the received documents is what that mode exists for, and `parseDocumentChange` keeps
|
||||
* each one within the limit, so readiness is not required while the mode is active.
|
||||
*/
|
||||
private get acceptsResultApplication() {
|
||||
if (this.services.appLifecycle.isReady()) return true;
|
||||
if (this.context.currentSettings().maxMTimeForReflectEvents <= 0) return false;
|
||||
// A fetch resets the local database, and a remote which reflects while fetching leaves this
|
||||
// processor unsuspended throughout. A document applied then cannot gather its chunks and is
|
||||
// dropped, so the database itself must still be usable.
|
||||
return this.services.database.isDatabaseReady();
|
||||
}
|
||||
|
||||
public get isSuspended() {
|
||||
return (
|
||||
this._suspended ||
|
||||
!this.services.appLifecycle.isReady() ||
|
||||
!this.acceptsResultApplication ||
|
||||
this.context.currentSettings().suspendParseReplicationResult ||
|
||||
this.services.appLifecycle.isSuspended()
|
||||
);
|
||||
|
||||
@@ -32,6 +32,8 @@ function note(id: string): PouchDB.Core.ExistingDocument<EntryDoc> {
|
||||
|
||||
type SetupOptions = {
|
||||
applicationReady?: boolean;
|
||||
databaseReady?: boolean;
|
||||
maxMTimeForReflectEvents?: number;
|
||||
isValidPath?: (path: string) => boolean;
|
||||
processSynchroniseResult?: (entry: unknown) => Promise<boolean>;
|
||||
setSnapshot?: (key: string, value: unknown) => Promise<unknown>;
|
||||
@@ -48,6 +50,7 @@ function setup(options: SetupOptions = {}) {
|
||||
const core = {
|
||||
services: {
|
||||
appLifecycle: { isReady, isSuspended: () => false },
|
||||
database: { isDatabaseReady: () => options.databaseReady ?? true },
|
||||
path: { getPath: (entry: { path: string }) => entry.path },
|
||||
replication: {
|
||||
databaseQueueCount: reactiveSource(0),
|
||||
@@ -71,7 +74,10 @@ function setup(options: SetupOptions = {}) {
|
||||
},
|
||||
};
|
||||
const processor = new ReplicateResultProcessor({
|
||||
currentSettings: () => ({ maxMTimeForReflectEvents: 0, suspendParseReplicationResult: false }),
|
||||
currentSettings: () => ({
|
||||
maxMTimeForReflectEvents: options.maxMTimeForReflectEvents ?? 0,
|
||||
suspendParseReplicationResult: false,
|
||||
}),
|
||||
getKeyValueDB: () => core.kvDB,
|
||||
getLocalDatabase: () => core.localDatabase,
|
||||
requestActiveReplicatorRetirement: () => {
|
||||
@@ -151,6 +157,45 @@ describe("ReplicateResultProcessor", () => {
|
||||
expect(isReady).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("applies results in remediation mode, which never reports readiness", () => {
|
||||
const { processor } = setup({
|
||||
applicationReady: false,
|
||||
maxMTimeForReflectEvents: Date.parse("2026-09-01T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(processor.isSuspended).toBe(false);
|
||||
});
|
||||
|
||||
it("holds results in remediation mode while the local database is being rebuilt", () => {
|
||||
const { processor } = setup({
|
||||
applicationReady: false,
|
||||
databaseReady: false,
|
||||
maxMTimeForReflectEvents: Date.parse("2026-09-01T00:00:00Z"),
|
||||
});
|
||||
|
||||
expect(processor.isSuspended).toBe(true);
|
||||
});
|
||||
|
||||
it("still skips a document modified after the limit while the application is unready", async () => {
|
||||
const maxMTimeForReflectEvents = Date.parse("2026-09-01T00:00:00Z");
|
||||
const { processor, processSynchroniseResult } = setup({
|
||||
applicationReady: false,
|
||||
maxMTimeForReflectEvents,
|
||||
});
|
||||
|
||||
const tooRecent = {
|
||||
...note("too-recent"),
|
||||
mtime: maxMTimeForReflectEvents + 1,
|
||||
} as PouchDB.Core.ExistingDocument<EntryDoc>;
|
||||
processor.enqueueAll([tooRecent]);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(processor["_queuedChanges"]).toHaveLength(0);
|
||||
expect(processor["_processingChanges"]).toHaveLength(0);
|
||||
});
|
||||
expect(processSynchroniseResult).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retires active ownership when a newer remote version is observed", async () => {
|
||||
const { onCloseActiveReplication, processor } = setup();
|
||||
const versionInfo = {
|
||||
|
||||
@@ -51,6 +51,7 @@ export function useReplicationFeature<TContext extends ServiceContext, TCommands
|
||||
: await task(),
|
||||
services: {
|
||||
appLifecycle: services.appLifecycle,
|
||||
database: services.database,
|
||||
path: services.path,
|
||||
replication: services.replication,
|
||||
vault: services.vault,
|
||||
|
||||
@@ -18,6 +18,8 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
||||
|
||||
- Files with colons in their names now retain their full paths in synchronisation data instead of appearing as incorrectly named copies at the Vault root. (#1206)
|
||||
- Obsidian may refuse to create a missing file with such a name. LiveSync also treats these names as invalid on Windows and Android, so the file may not appear in those devices' Vaults. Existing misplaced copies are left for you to review; this change does not remove them automatically.
|
||||
- Received changes are applied again while remediation mode is active. That mode prevents the scan which readiness depends upon, so nothing had been applied since the plug-in began waiting for readiness, not even changes older than the configured modification-time limit. The limit itself is still enforced for every change, and application waits for a usable local database so that a change arriving during a fetch is not dropped.
|
||||
- A scheduled fetch no longer offers Simple Fetch while remediation mode is active. Simple Fetch reconciles the Vault with the local database past the restriction, which could store the current files or apply changes newer than the limit; the detailed flow states the restriction and offers to clear it first (#1202).
|
||||
|
||||
## 1.0.30
|
||||
|
||||
|
||||
Reference in New Issue
Block a user