Repository navigation
fix(sync): show a synced library that Obsidian does not report as changed - #480
Merged
Merged
Conversation
… changed Obsidian calls onExternalSettingsChange only when data.json's modified time is later than the plugin's last saveData. A sync client that keeps the time the other device saved the file, or a write that rounds to the same millisecond as a save, is never reported, so a new device keeps its empty first-run library (#477). The e2e test moves a library over data.json with an earlier modified time, the way such a sync client does, and expects the view to show it. The unit tests take a change reported as a raw vault event once the reports settle, take a change that landed while PodNotes loaded, and read nothing after unload. The own-save e2e test now also counts writes, so a check of PodNotes' own save that writes again fails it.
PodNotes learned of a data.json replaced by sync only through onExternalSettingsChange, which Obsidian skips unless the file's modified time is later than the plugin's last save. A new device that saved its first-run library before sync finished never showed the synced library: with a sync client that keeps the other device's earlier modified time always, and in the e2e journey once in about 300 runs, when the test's write rounded to the save's millisecond (#477). PodNotes now listens to the vault's raw event for data.json, which Obsidian raises for every change, PodNotes' own writes included, and checks the file 50 ms after the changes stop, so a file written in place is read whole. The writer's existing comparison with what was last loaded or written decides whether anything changed, so an own save changes nothing. One check also runs after load, for a change that landed before the listener existed. A read that finds the file unchanged skips decoding it, so the extra check costs a parse and a comparison. onExternalSettingsChange stays: Obsidian refreshes an open settings tab once it resolves.
Deploying podnotes with
|
| Latest commit: |
a58c9c6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://917f50c4.podnotes.pages.dev |
| Branch Preview URL: | https://fix-sync-adoption.podnotes.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #477
What users get
A phone or laptop that starts PodNotes before sync finishes now shows the synced library as soon as sync replaces
data.json, without a restart. Before this, Obsidian never told PodNotes about some of those replacements, so the new device kept showing "Add your first podcast" until something else saved or the app restarted. The merge that keeps the library safe is unchanged: the new device still never writes its empty first-run state over the synced library.Root cause
PodNotes learned that
data.jsonchanged elsewhere only throughPlugin#onExternalSettingsChange. Obsidian 1.14.4 calls it from a 50 ms debounced check that compares the file's modified time, rounded to the millisecond, with theDate.now()it recorded at the plugin's lastsaveData, and calls back only when the file is strictly newer. Two cases fail that check:So the verdict is a product race, not test timing. The journey waits on the right signal: the device must merge the library and keep its own change.
Fix
main.tslistens to the vault'srawevent fordata.json. Obsidian raises it for every change to the file, PodNotes' own writes included, and uses it to driveonExternalSettingsChangeitself. One check also runs right after load, for a change that landed before the listener existed.DataFileWriter.report()checks the file 50 ms after the changes stop, so a file written in place is read whole. The writer's existing comparison with what it last loaded or wrote decides whether anything changed. An own save changes nothing.dataFileAccess.readreturns early when the file holds exactly what PodNotes last loaded or wrote, so the extra check after each save costs a JSON parse and a comparison, not a decode.onExternalSettingsChangestays, because Obsidian refreshes an open settings tab once it resolves.global.d.tstypes the undocumentedrawevent.How I verified it
Loop statistics. The #477 journey was instrumented (raw events, Obsidian's stats and
_lastDataModifiedTime,onExternalSettingsChange,loadData/saveData,utimes, writer state and notices) and repeated on this worktree's isolated instance under CPU contention (16 busy loops on 12 cores). Obsidian 1.14.4, Linux, XFS,CONFIG_HZ=1000.The first attempt read the file on every raw event. It read data.json mid-write twice, raised the "could not read" refusal, and the refusal made an unloading instance flush its old library back. That is why the check now waits 50 ms after the changes stop, the same wait Obsidian uses.
Deterministic reproduction, the earlier-mtime sync written by the new e2e test:
Real surface (verify skill,
.obsidian-e2e-artifacts/sync-adoption/). The journey is a new install, a screenshot, then a sync-style move of the library with a time one minute earlier, then a second screenshot.dev:errorsis empty.Writes.
npm run count:writeson the normal session from V-475 (play 12 s, pause, seek to 30, play 2 s, pause), two sessions per build: master 3 attempted, 3 completed, 0 failed, twice; this fix 3, 3, 0, twice.Mutations
Each plausible wrong version of the fix, and the test that fails on it:
vault.on("modify")instead ofraw(config files are not vault files)rawreloads the file into memory without the mergerawgated by Obsidian's own mtime comparisondata.jsonOne mutant survives. A timer that fires 50 ms after the first report instead of the last, which is Obsidian's own debounce, passes every test. It differs only for a writer that pauses more than 50 ms mid-write.
Checks
Node v22.23.2, on head
a58c9c6c:npm run lint: 0 errors, 0 warnings; Svelte policies 0 errors, 0 warnings.npm run format:check: all matched files use the correct format.npm run typecheck: passes.npm run build: passes.npm run test: 2,579 tests passed in 122 files.npm run docs:build: 18 pages; all internal links valid; all 112 legacy fragments resolve.npm run test:tooling: 13 tests passed in 2 files.npm run test:e2eon a fresh vault: 193 tests passed in 26 files (767.5 s).npm run size: raw 312,730 bytes (budget 330,000), gzip 99,143 bytes (budget 100,000). Against a master build in the same environment, the bundle grows by 479 raw bytes and 89 gzip bytes (gzip -9).sync-persistence.test.tson its own: 11 of 11 on a fresh vault.The test commit lands first and fails on master; the fix commit follows.
Docs at release
docs/src/content/docs/sync.md, wherever the release describes PodNotes picking up adata.jsonthat sync replaces without a restart: say it holds for sync services that keep the file's original modified time too.Follow-ups
writeFileandutimesfails its read-back and shows "PodNotes could not save your changes." although nothing was lost. The file is now adopted at once rather than after the 10 s retry. The false notice remains, in 17 of the 300 loop runs, where the test writes right after PodNotes does. Real sync rarely lands that close.onExternalSettingsChange(the cases above), it also does not refresh an open PodNotes settings tab, so the tab shows the old values until it is reopened. That predates this PR.rawevent fordata.json, PodNotes behaves there as before.saveDatarecordsDate.now()before the write, and its check needs a strictly newer modified time, so synced files that keep their time are never reported to any plugin.Note
Fix external data.json changes that Obsidian only reports as raw vault events
Fixes a bug where a synced library written by another device was not loaded because Obsidian did not report the data.json change through its normal settings channel.
REPORT_SETTLE_MS(50 ms) toDataFileWriterin writer.ts. Reported file changes are coalesced and the file is read once, 50 ms after the last report. Unload cancels pending reports.PodNotes.dataFileAccessgains a fast path: when loaded data is JSON-value-equal to the writer's base document, it returns a clone of the base instead of falling through to the unreadable/missing/document validation paths.Macroscope summarized a58c9c6.