Queued replication results are not resumed when the application becomes ready
Rewritten at your request to cover only the queued replication results. Remediation mode, including the Fetch and Rebuild failures and the conflict-resolution caveat, is now #1202.
This is a code-level report, based on reading the source and on unit tests, so it does not follow the issue template: there is no device debug info or LiveSync report to attach.
Revisions
- Found in a security fork of LiveSync 1.0.21 (upstream base f5f7aab11f03f62c6946d2fa296c50bb5df5b2a4) with Commonlib 0.1.19 (upstream base e8beaf017de243ee3d4beb998905a51de30ebb44), and confirmed there with unit tests.
- Re-checked against upstream
mainon 2026-09-17: LiveSync c60323e1 (1.0.29) and Commonlib 26efbb08 (0.1.26). The code paths below are the current ones. - Not reproduced on a released 1.0.28 or 1.0.29 build.
Summary
1d2077d3 (1.0.23) correctly made ReplicateResultProcessor.isSuspended call appLifecycle.isReady(). That is right, and it exposed a gap the previous, always-false check had hidden: nothing starts the processing queue when readiness is established later, so documents queued while the application is not ready wait for an unrelated event.
markIsReady() only sets a flag (livesync-commonlib/src/services/base/AppLifecycleService.ts:160). The queue is started by enqueueChange for a newly arrived document (src/serviceFeatures/replication/ReplicateResultProcessor.ts:326) or by resume() (:108), and resume() is reachable only through resumeResultApplication on EVENT_SETTING_SAVED (src/serviceFeatures/replication/automaticTriggers.ts:55-62). Readiness itself starts nothing.
1. Snapshot restoration at start-up
The processor restores its snapshot on onDatabaseInitialised (src/serviceFeatures/replication/index.ts:100), which runs inside prepareDatabaseForUse before markIsReady() (livesync-commonlib/src/serviceFeatures/prepareDatabaseForUse.ts:74 and :81). The restored documents are queued while the application is not yet ready, and runProcessQueue returns immediately (ReplicateResultProcessor.ts:377).
Nothing re-runs the queue when readiness is marked a few lines later, so those documents wait for the next replicated change or the next settings save. In a quiet Vault that can be indefinitely, and the same race repeats on every start.
2. Fetch completion
Rebuilder.finishRebuild() resets readiness, resumes reflection, then calls saveSettingData() and only afterwards markIsReady() (livesync-commonlib/src/serviceModules/Rebuilder.ts:595-614). Two things follow:
- The settings save fires
EVENT_SETTING_SAVED, soresumeResultApplicationruns while the application is still not ready;runProcessQueuereturns at once, and the latermarkIsReady()does not try again. - The save happens only when
controlsReflectionis true. WithdoNotSuspendOnFetching, or for an Object Storage remote whenignoreMinIOis false, there is no settings save at all, so nothing even attempts a resume.
Fetched documents therefore stay queued until something unrelated arrives.
What we did in the fork
ReplicateResultProcessor.resumeAfterApplicationReady() refreshes the local application activity and starts the queue without lifting an explicit suspend() or suspendParseReplicationResult, and the host wraps services.appLifecycle.markIsReady so the original runs first and held documents are resumed afterwards. That covers every Commonlib caller (prepareDatabaseForUse, completePreparedRebuild, finishRebuild) without a Commonlib change.
Wrapping a service method is not something I would propose upstream. A readiness event or hook in Commonlib, which the host can subscribe to, looks like the cleaner place; I am happy to shape it whichever way you prefer.
Regression tests
In the fork, at kimjansheden/obsidian-livesync@f8d389bb (base f5f7aab1, Commonlib 0.1.19-security.8):
src/modules/core/ModuleReplicator.unit.spec.ts, using Commonlib's real InjectableAppLifecycleService:
applies documents restored before readiness once the application becomes ready— fails with the one-lineisReady()correction alone, which is the stall in section 1applies documents restored during database preparation once Commonlib marks readiness— a contract test which runs Commonlib's exportedprepareDatabaseForUseand checks that restored documents are not applied before readiness and are applied afterwardskeeps an explicit reflection suspension when the application becomes readykeeps a suspended processor suspended when the application becomes ready
src/modules/core/ReplicateResultProcessor.unit.spec.ts:
holds replicated documents while the application is not readycontinues held documents after readiness without lifting an explicit suspension
Removing the resume fails both readiness tests; making the wrapper call resume() instead fails the suspension test. The mock exposes isReady as a function, as the real service does; a boolean property hid the original defect.
The fork's paths differ from current upstream (src/modules/core/ rather than src/serviceFeatures/replication/), so the tests need porting, which I am happy to do.
Related, not part of this issue
The same referenced-not-called pattern that 1d2077d3 fixed still exists in two places in src/features/ConfigSync/CmdConfigSync.ts on main: if (!this._isMainReady) in _everyRealizeSettingSyncMode and in watchVaultRawEventsAsync. A third line in the same handler, if (!this._isMainSuspended()) return true;, inverts the pre-0.24 suspension check. I have fixes and tests for both in the fork and can file a separate issue or a PR if you want them.
Offer
I can open a focused PR for this issue in whichever shape you prefer: a readiness event in Commonlib with the plug-in subscribing to it, linked across both repositories, or a plug-in-only change if you would rather not extend the Commonlib contract yet.
Source: vrtmrz/obsidian-livesync