fix: keep replicated document application active

Why:
- Large downloaded batches can outlive the finite remote operation and be suspended before every document reaches the Vault.
- The v1.0 activity contract requires local application to remain distinct from remote-operation reporting.

Changes:
- Add an Obsidian-owned local application activity boundary which reuses Wake Lock policy without incrementing remote activity.
- Retain one boundary through queue processing and the final recovery snapshot, releasing and reacquiring it around suspension.
- Log final snapshot failures, preserve processing results, and cover the lifecycle with focused tests and documentation.
This commit is contained in:
Ouyang Xingyuan
2026-07-31 11:06:18 +08:00
parent c385bd7ce7
commit 0df1f57f3b
8 changed files with 305 additions and 31 deletions
@@ -24,6 +24,7 @@ function setup(opts: SetupOptions) {
};
const fileProcessing = { commitPendingFileEvents: vi.fn(async () => true) };
const boundedRemoteActivityCount = reactiveSource(0);
const boundedLocalApplicationActivityCount = reactiveSource(0);
const core = {
_services: {
@@ -38,7 +39,7 @@ function setup(opts: SetupOptions) {
setting: { saveSettingData: vi.fn(async () => undefined) },
appLifecycle,
fileProcessing,
replicator: { boundedRemoteActivityCount },
replicator: { boundedRemoteActivityCount, boundedLocalApplicationActivityCount },
},
settings: {
...DEFAULT_SETTINGS,
@@ -56,7 +57,13 @@ function setup(opts: SetupOptions) {
// The handler reads `activeWindow.document.hidden`.
(globalThis as any).activeWindow = { document: { hidden: opts.hidden } };
return { module, appLifecycle, fileProcessing, boundedRemoteActivityCount };
return {
module,
appLifecycle,
fileProcessing,
boundedRemoteActivityCount,
boundedLocalApplicationActivityCount,
};
}
describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", () => {
@@ -109,6 +116,21 @@ describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", ()
await vi.waitFor(() => expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1));
});
it("defers suspension while local document application is active", async () => {
const { module, appLifecycle, boundedLocalApplicationActivityCount } = setup({
settings: { keepReplicationActiveInBackground: false, liveSync: false },
hidden: true,
});
boundedLocalApplicationActivityCount.value = 1;
await module.watchWindowVisibilityAsync();
expect(appLifecycle.onSuspending).not.toHaveBeenCalled();
boundedLocalApplicationActivityCount.value = 0;
await vi.waitFor(() => expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1));
});
it("defers mobile suspension while bounded remote activity is running", async () => {
const { module, appLifecycle, boundedRemoteActivityCount } = setup({
settings: { keepReplicationActiveInBackground: false, liveSync: false },