mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-03 14:58:18 +00:00
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:
1 parent
73a14d4e4b
commit
68fa04af3c
9 files changed
+1512
-15
No files matched your search
@@ -230,7 +230,13 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
|
||||
}
|
||||
this.isLastHidden = isHidden;
|
||||
|
||||
await this.services.fileProcessing.commitPendingFileEvents();
|
||||
const pendingFileEventsCommitted = await this.services.fileProcessing.commitPendingFileEvents();
|
||||
if (!pendingFileEventsCommitted) {
|
||||
this._log(
|
||||
"Pending file events remain because their commit did not complete before the window visibility changed.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop opt-in (LiveSync/Periodic only): keep the background channel running while the
|
||||
// window is hidden, instead of suspending on hide. On hide we skip the suspend for both
|
||||
@@ -287,7 +293,10 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.settings.isConfigured) return;
|
||||
if (!this.services.appLifecycle.isReady()) return;
|
||||
await this.services.fileProcessing.commitPendingFileEvents();
|
||||
const pendingFileEventsCommitted = await this.services.fileProcessing.commitPendingFileEvents();
|
||||
if (!pendingFileEventsCommitted) {
|
||||
return;
|
||||
}
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { ModuleObsidianEvents } from "./ModuleObsidianEvents";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
|
||||
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
|
||||
|
||||
type SetupOptions = {
|
||||
settings?: Partial<typeof DEFAULT_SETTINGS>;
|
||||
@@ -23,6 +24,8 @@ function setup(opts: SetupOptions) {
|
||||
onResumed: vi.fn(async () => true),
|
||||
};
|
||||
const fileProcessing = { commitPendingFileEvents: vi.fn(async () => true) };
|
||||
const replication = { replicateUnattendedByEvent: vi.fn(async () => undefined) };
|
||||
const conflict = { queueCheckForIfOpen: vi.fn(async () => undefined) };
|
||||
const boundedRemoteActivityCount = reactiveSource(0);
|
||||
const boundedLocalApplicationActivityCount = reactiveSource(0);
|
||||
|
||||
@@ -39,6 +42,8 @@ function setup(opts: SetupOptions) {
|
||||
setting: { saveSettingData: vi.fn(async () => undefined) },
|
||||
appLifecycle,
|
||||
fileProcessing,
|
||||
replication,
|
||||
conflict,
|
||||
replicator: { boundedRemoteActivityCount, boundedLocalApplicationActivityCount },
|
||||
},
|
||||
settings: {
|
||||
@@ -61,6 +66,9 @@ function setup(opts: SetupOptions) {
|
||||
module,
|
||||
appLifecycle,
|
||||
fileProcessing,
|
||||
replication,
|
||||
conflict,
|
||||
api: core._services.API,
|
||||
boundedRemoteActivityCount,
|
||||
boundedLocalApplicationActivityCount,
|
||||
};
|
||||
@@ -303,6 +311,23 @@ describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", ()
|
||||
expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("logs a failed pending-file commit and still suspends on hide", async () => {
|
||||
const { module, appLifecycle, fileProcessing, api } = setup({
|
||||
settings: { keepReplicationActiveInBackground: false, liveSync: true },
|
||||
hidden: true,
|
||||
});
|
||||
fileProcessing.commitPendingFileEvents.mockResolvedValue(false);
|
||||
|
||||
await module.watchWindowVisibilityAsync();
|
||||
|
||||
expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1);
|
||||
expect(api.addLog).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Pending file events remain"),
|
||||
LOG_LEVEL_NOTICE,
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
it("does not apply on mobile even if the flag is set", async () => {
|
||||
const { module, appLifecycle } = setup({
|
||||
settings: { keepReplicationActiveInBackground: true, liveSync: true },
|
||||
@@ -313,3 +338,20 @@ describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", ()
|
||||
expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("watchWorkspaceOpenAsync — pending file event commit", () => {
|
||||
it("skips replication and conflict checks when pending file events fail to commit", async () => {
|
||||
const { module, fileProcessing, replication, conflict } = setup({
|
||||
settings: { syncOnFileOpen: true },
|
||||
hidden: false,
|
||||
});
|
||||
fileProcessing.commitPendingFileEvents.mockResolvedValue(false);
|
||||
|
||||
await module.watchWorkspaceOpenAsync({ path: "note.md" } as Parameters<
|
||||
typeof module.watchWorkspaceOpenAsync
|
||||
>[0]);
|
||||
|
||||
expect(replication.replicateUnattendedByEvent).not.toHaveBeenCalled();
|
||||
expect(conflict.queueCheckForIfOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user