mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-13 14:17:04 +00:00
Warn when live Vault reflection fails
This commit is contained in:
@@ -25,6 +25,7 @@ import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||
import type PouchDB from "pouchdb-core";
|
||||
import { promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { $msg } from "@/common/translation";
|
||||
|
||||
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
|
||||
const REPROCESS_BATCH_SIZE = 100;
|
||||
@@ -79,6 +80,14 @@ export class ReplicateResultProcessor {
|
||||
private logError(e: unknown) {
|
||||
Logger(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
private reportVaultReflectionFailure(entry: MetaEntry, cause?: unknown) {
|
||||
this.log(
|
||||
`Live replication could not reflect ${this.getPath(entry)} from the local database to the Vault; this path remains eligible for a later Vault scan.`,
|
||||
LOG_LEVEL_VERBOSE
|
||||
);
|
||||
if (cause !== undefined) this.logError(cause);
|
||||
Logger($msg("Ui.Common.SomeFilesCouldNotBeSynchronised"), LOG_LEVEL_NOTICE);
|
||||
}
|
||||
constructor(private readonly context: ReplicateResultProcessorContext) {}
|
||||
|
||||
private get localDatabase() {
|
||||
@@ -510,8 +519,16 @@ export class ReplicateResultProcessor {
|
||||
this.log(`Processed by other processor: ${docNote}`, LOG_LEVEL_DEBUG);
|
||||
} else if (this.services.vault.isValidPath(this.getPath(doc))) {
|
||||
// Apply to storage if the path is valid
|
||||
await this.applyToStorage(doc as MetaEntry);
|
||||
this.log(`Processed: ${docNote}`, LOG_LEVEL_DEBUG);
|
||||
try {
|
||||
const reflected = await this.applyToStorage(doc as MetaEntry);
|
||||
if (!reflected) {
|
||||
this.reportVaultReflectionFailure(doc as MetaEntry);
|
||||
return;
|
||||
}
|
||||
this.log(`Processed: ${docNote}`, LOG_LEVEL_DEBUG);
|
||||
} catch (error) {
|
||||
this.reportVaultReflectionFailure(doc as MetaEntry, error);
|
||||
}
|
||||
} else {
|
||||
// Should process, but have an invalid path
|
||||
this.log(`Unprocessed (Invalid path): ${docNote}`, LOG_LEVEL_VERBOSE);
|
||||
@@ -525,9 +542,10 @@ export class ReplicateResultProcessor {
|
||||
* @returns
|
||||
*/
|
||||
protected applyToStorage(entry: MetaEntry) {
|
||||
return this.withCounting(async () => {
|
||||
await this.services.replication.processSynchroniseResult(entry);
|
||||
}, this.services.replication.storageApplyingCount);
|
||||
return this.withCounting(
|
||||
() => this.services.replication.processSynchroniseResult(entry),
|
||||
this.services.replication.storageApplyingCount
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,13 @@ import { promiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { VER, type EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
defaultLogger,
|
||||
LOG_LEVEL_DEBUG,
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
setGlobalLogFunction,
|
||||
} from "octagonal-wheels/common/logger";
|
||||
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
|
||||
|
||||
function note(id: string): PouchDB.Core.ExistingDocument<EntryDoc> {
|
||||
@@ -21,12 +28,12 @@ function note(id: string): PouchDB.Core.ExistingDocument<EntryDoc> {
|
||||
|
||||
type SetupOptions = {
|
||||
applicationReady?: boolean;
|
||||
processSynchroniseResult?: (entry: unknown) => Promise<void>;
|
||||
processSynchroniseResult?: (entry: unknown) => Promise<boolean>;
|
||||
setSnapshot?: (key: string, value: unknown) => Promise<unknown>;
|
||||
};
|
||||
|
||||
function setup(options: SetupOptions = {}) {
|
||||
const processSynchroniseResult = vi.fn(options.processSynchroniseResult ?? (async () => undefined));
|
||||
const processSynchroniseResult = vi.fn(options.processSynchroniseResult ?? (async () => true));
|
||||
const setSnapshot = vi.fn(options.setSnapshot ?? (async () => undefined));
|
||||
const runBoundedLocalApplicationActivity = vi.fn(async (task: () => Promise<void>) => await task());
|
||||
const onCloseActiveReplication = vi.fn(async () => true);
|
||||
@@ -120,7 +127,7 @@ describe("ReplicateResultProcessor", () => {
|
||||
});
|
||||
|
||||
it("keeps one local application activity until every replicated document has been applied", async () => {
|
||||
const applying = promiseWithResolvers<void>();
|
||||
const applying = promiseWithResolvers<boolean>();
|
||||
let activityFinished = false;
|
||||
const { processor, processSynchroniseResult, runBoundedLocalApplicationActivity } = setup({
|
||||
processSynchroniseResult: async () => applying.promise,
|
||||
@@ -139,7 +146,7 @@ describe("ReplicateResultProcessor", () => {
|
||||
});
|
||||
expect(activityFinished).toBe(false);
|
||||
|
||||
applying.resolve();
|
||||
applying.resolve(true);
|
||||
|
||||
await vi.waitFor(() => expect(activityFinished).toBe(true));
|
||||
});
|
||||
@@ -160,7 +167,7 @@ describe("ReplicateResultProcessor", () => {
|
||||
});
|
||||
|
||||
it("releases and reacquires local application activity around processing suspension", async () => {
|
||||
const applying = promiseWithResolvers<void>();
|
||||
const applying = promiseWithResolvers<boolean>();
|
||||
let completedActivities = 0;
|
||||
const { processor, processSynchroniseResult, runBoundedLocalApplicationActivity } = setup({
|
||||
processSynchroniseResult: async () => applying.promise,
|
||||
@@ -178,7 +185,47 @@ describe("ReplicateResultProcessor", () => {
|
||||
processor.resume();
|
||||
await vi.waitFor(() => expect(runBoundedLocalApplicationActivity).toHaveBeenCalledTimes(2));
|
||||
|
||||
applying.resolve();
|
||||
applying.resolve(true);
|
||||
await vi.waitFor(() => expect(completedActivities).toBe(2));
|
||||
});
|
||||
|
||||
it.each([
|
||||
["returns false", async () => false, undefined],
|
||||
["throws", async () => Promise.reject(new Error("File name too long")), "File name too long"],
|
||||
])("reports when Vault reflection %s", async (_description, processSynchroniseResult, errorMessage) => {
|
||||
const log = vi.fn((_message: unknown, _level?: number) => undefined);
|
||||
setGlobalLogFunction(log);
|
||||
try {
|
||||
const { processor } = setup({ processSynchroniseResult });
|
||||
|
||||
processor.enqueueAll([note("unreflectable")]);
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
"Not all files could be synchronised. Check the affected files. Generate a report to review the detailed log.",
|
||||
LOG_LEVEL_NOTICE,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
"[ReplicateResultProcessor] Live replication could not reflect unreflectable.md from the local database to the Vault; this path remains eligible for a later Vault scan.",
|
||||
LOG_LEVEL_VERBOSE,
|
||||
undefined
|
||||
);
|
||||
if (errorMessage !== undefined) {
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: errorMessage }),
|
||||
LOG_LEVEL_VERBOSE,
|
||||
undefined
|
||||
);
|
||||
}
|
||||
expect(log).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("Processed: unreflectable.md"),
|
||||
LOG_LEVEL_DEBUG,
|
||||
undefined
|
||||
);
|
||||
} finally {
|
||||
setGlobalLogFunction(defaultLogger);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user