Expose explicit LiveSync batching and honour save failures

Show the batching choice for the current synchronisation mode while
preserving both mode preferences. Observe local save failures before
file-open work and report failed asynchronous unload preparation.

Record the accepted recovery and batching contract, update the primary
settings documentation, and add two-device runtime coverage for batching,
deletions, saved settings, and interrupted local processing.

Requires Commonlib commit 72bdc66af5d7338d8f2f69ebe83c3866def93360.
Update the exact registry dependency when its package version is published.

Validation: full repository check; latest packed dependency type checks,
production build, 1,373 unit tests (one skipped), and six focused runtime
recovery cases. The earlier implementation passed the full 23-case runtime
comparison and five additional interruption conditions.
This commit is contained in:
vorotamoroz committed 2026-10-25 02:47:55 +00:00
1 parent 73a14d4e4b
commit 68fa04af3c
9 files changed
+1512 -15

No files matched your search

@@ -171,20 +171,18 @@ export function paneSyncSettings(
}
});
void addPanel(
paneEl,
$msg("obsidianLiveSyncSettingTab.titleUpdateThinning"),
undefined,
visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC"))
).then((paneEl) => {
new Setting(paneEl).autoWireToggle("batchSave");
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleUpdateThinning")).then((paneEl) => {
const inLiveSyncMode = () => this.isConfiguredAs("syncMode", "LIVESYNC");
const batchEnabled = () => this.isConfiguredAs(inLiveSyncMode() ? "batchSaveInLiveSync" : "batchSave", true);
new Setting(paneEl).autoWireToggle("batchSave", { onUpdate: visibleOnly(() => !inLiveSyncMode()) });
new Setting(paneEl).autoWireToggle("batchSaveInLiveSync", { onUpdate: visibleOnly(inLiveSyncMode) });
new Setting(paneEl).autoWireNumeric("batchSaveMinimumDelay", {
acceptZero: true,
onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)),
onUpdate: visibleOnly(batchEnabled),
});
new Setting(paneEl).autoWireNumeric("batchSaveMaximumDelay", {
acceptZero: true,
onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)),
onUpdate: visibleOnly(batchEnabled),
});
});
@@ -0,0 +1,98 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import type { OnUpdateFunc, PageFunctions } from "./SettingPane";
const controls = vi.hoisted(() => new Map<string, { onUpdate?: () => { visibility?: boolean } }>());
vi.mock("@/common/translation", () => ({ $msg: (key: string) => key }));
vi.mock("@vrtmrz/livesync-commonlib/compat/common/logger", () => ({ Logger: vi.fn() }));
vi.mock("./LiveSyncSetting.ts", () => ({
LiveSyncSetting: class {
autoWireDropDown(key: string, options = {}) {
controls.set(key, options);
return this;
}
autoWireToggle(key: string, options = {}) {
controls.set(key, options);
return this;
}
autoWireNumeric(key: string, options = {}) {
controls.set(key, options);
return this;
}
},
}));
import { paneSyncSettings } from "./PaneSyncSettings";
function setup(liveSync: boolean, batchSave: boolean, batchSaveInLiveSync: boolean) {
const callbacks = new Map<string, (value: string) => Promise<void>>();
const panels = new Map<string, OnUpdateFunc | undefined>();
const tab = {
editingSettings: { ...DEFAULT_SETTINGS, liveSync, batchSave, batchSaveInLiveSync },
services: { API: { isMobile: () => false }, control: { applySettings: vi.fn(async () => {}) } },
addOnSaved: (key: string, callback: (value: string) => Promise<void>) => callbacks.set(key, callback),
isConfiguredAs(key: string, value: unknown) {
if (key === "syncMode") {
return (
(this.editingSettings.liveSync
? "LIVESYNC"
: this.editingSettings.periodicReplication
? "PERIODIC"
: "ONEVENTS") === value
);
}
return this.editingSettings[key as keyof typeof this.editingSettings] === value;
},
saveSettings: vi.fn(async () => {}),
requestCatalogueRefresh: vi.fn(),
};
const functions = {
addPanel: (_element: HTMLElement, title: string, _callback: unknown, onUpdate?: OnUpdateFunc) => ({
then(callback: (element: HTMLElement) => void) {
if (!title.endsWith("titleSynchronizationMethod") && !title.endsWith("titleUpdateThinning")) return;
panels.set(title, onUpdate);
callback({} as HTMLElement);
},
}),
} as unknown as PageFunctions;
paneSyncSettings.call(tab as unknown as ObsidianLiveSyncSettingTab, {} as HTMLElement, functions);
return { tab, callbacks, panels };
}
function visible(key: string): boolean {
const control = controls.get(key);
if (!control) throw new Error(`Missing setting control: ${key}`);
return control.onUpdate?.().visibility !== false;
}
beforeEach(() => controls.clear());
describe("mode-specific database batching settings", () => {
it.each([
{ liveSync: true, batchSave: true, batchSaveInLiveSync: false, delays: false },
{ liveSync: true, batchSave: false, batchSaveInLiveSync: true, delays: true },
{ liveSync: false, batchSave: true, batchSaveInLiveSync: false, delays: true },
{ liveSync: false, batchSave: false, batchSaveInLiveSync: true, delays: false },
])("shows the current mode choice and its shared delays: %j", ({ delays, ...settings }) => {
const { panels } = setup(settings.liveSync, settings.batchSave, settings.batchSaveInLiveSync);
expect(panels.get("obsidianLiveSyncSettingTab.titleUpdateThinning")?.().visibility).not.toBe(false);
expect(visible("batchSave")).toBe(!settings.liveSync);
expect(visible("batchSaveInLiveSync")).toBe(settings.liveSync);
expect(visible("batchSaveMinimumDelay")).toBe(delays);
expect(visible("batchSaveMaximumDelay")).toBe(delays);
});
it.each([
[true, false],
[false, true],
])("retains both batching choices across mode changes: %j", async (batchSave, batchSaveInLiveSync) => {
const { tab, callbacks } = setup(false, batchSave, batchSaveInLiveSync);
const switchMode = callbacks.get("syncMode")!;
for (const mode of ["LIVESYNC", "PERIODIC", "ONEVENTS", "LIVESYNC"]) {
await switchMode(mode);
expect(tab.editingSettings).toMatchObject({ batchSave, batchSaveInLiveSync });
}
expect(tab.services.control.applySettings).toHaveBeenCalledTimes(4);
});
});