Repository navigation
fix(persistence): never let a device that started before sync wipe the synced library - #398
Merged
Merged
Conversation
…ed library A device that starts with no data.json writes a defaults file at once and saves its own changes on top. When that file reaches a device holding the synced library, the library device adopts it whole, because its memory equals its base and the merge takes disk. In the other race order the new device adopts the library and drops what its user added. Also reproduces a rollback undone when an external change reports the file a failed write reached.
…eal Obsidian PodNotes started with no data.json writes the file a new device holds before the sync client delivers the library. Delivering it over a library loses the library; delivering the library over it loses the new device's change.
… file a failed write reached A save settles a failed write whose content the file shows, so memory and base match what is on disk before it merges. A change reported elsewhere merged that file against the old base instead, so an edit made since the failed write, such as a failed import's rollback, was undone.
…re sync meets the synced data.json
A device that starts with no data.json writes a library of its own. When that
file reached a device holding the synced library, the merge read every
difference from its base as an edit made elsewhere and took the file whole, so
the library was gone on every device. In the other race order the new device
dropped what its user had added.
data.json now carries an optional root libraryId. A new install gets one that
sorts by when the library started; a file without one belongs to the library
every device shared before this change (""). A file whose libraryId differs
from the base is another library: the two merge from the empty library, so
every entry of both survives, and the merged file keeps the libraryId of the
library started first, so its other devices go on merging three ways. Every
decoded file carries a libraryId, so each save writes one and an Obsidian Sync
key merge cannot pair one device's id with the other's content.
An object both sides added, such as a playlist both libraries created under one
name, now merges per key instead of taking one side whole. A device that could
not read its file at startup starts a library of its own for the same reason.
The test-only DataFileWriter.hasBase getter is gone.
…was already settled A failed write that reached disk is settled when the file shows it, but stays recorded, so the next save settles the same snapshot again and brings back an entry removed since.
…d disk Settling a failed write whose content the file shows now also drops the record, in a save and in a change reported elsewhere alike. Kept, the record was settled again by the next save against memory from before the failure, which brought back an entry removed since.
The codec now writes the libraryId wherever it encodes a document, "" when the root fields have none, instead of relying on every caller to decode one first. A document built without them stays in its library, and the type says so. newLibraryId names the id a new library gets. The two-device tests assert that a device writes nothing once both agree, and the e2e helper checks that it produced a new device's file.
The codec cases build the file a released version wrote, without a key or with one that is not text, instead of letting the encoder fill it in first. The crossed-delivery test drops a delivery of a device's own file. The encoder notes why libraryId is not a known key.
…it relies on mergeDataFile names the empty library it merges two libraries from and keeps the library that started first; the library from before ids is a named constant. The test that memory, not base, decides which library a merge keeps stands on its own.
Deploying podnotes with
|
| Latest commit: |
baee866
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5340416e.podnotes.pages.dev |
| Branch Preview URL: | https://fix-fresh-device-sync.podnotes.pages.dev |
…ferent titles The library saved a podcast under an earlier title and a new device saved its current title, both with the same feed URL. After the two libraries merge, both tiles render, each opens the feed's episodes, Settings opens, and no error is thrown.
This was referenced Oct 11, 2026
This was referenced Oct 13, 2026
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.
Summary
A device that starts PodNotes before its sync client delivers
data.jsonno longer wipes the library on the user's other devices. This covers a new device, and one whose file was missing, empty or unreadable.Since #388, a device with no unsaved edits takes a changed
data.jsonwhole. A fresh install writes a defaults file as soon as PodNotes loads. When that file reached a device that held the synced library, the merge read every difference as an edit from another device, and every device lost its feeds, progress, playlists and preferences. In the other race order, the new device lost what its user had added. Obsidian Sync produces both orders (its conflict rules).The fix gives
data.jsonan optional rootlibraryId."", which sorts first.libraryIddiffers from the base is another library.mergeDataFilemerges the two from the defaults document, so every entry of both survives. The result keeps the id of the library that started first, so that library's other devices still merge three ways.This PR also settles the two open Macroscope threads on #388. Thread (b) is real and fixed. Interrogate also found a related #388 bug, also fixed: a failed write that was already settled got settled again and brought back a removed entry. Thread (a) cannot happen, because no release among the 96 tags assigns or clears a playlist's
currentEpisode.Verification
Obsidian 1.13.7 ran on headless Linux in the isolated worktree instance, with two vaults. Vault A held a library: 2 feeds, 2 progress records, a playlist, a queue, favorites, playback rate 1.5 and a custom template. Vault B was a fresh install that added
http://127.0.0.1.nip.io:20784/feed-b.xmlin Settings. A script copieddata.jsonbetween the vaults the way a sync client does.each_key_duplicate) and passes with fix(feeds): show every podcast and episode, and keep feed lists correct while they load #399's grids.npm run lint,format:check,typecheck,build,test(1453 passed) anddocs:buildpass on the head.npm run test:e2epassed 61 of 61, including fix(persistence): merge data.json with changes made on other devices instead of overwriting them #388's sync cases.npm run sizereports raw 315,027 bytes and gzip 99,505 bytes, +163 over master's 99,342, with 495 bytes of gzip headroom left. The test-onlyDataFileWriter.hasBasegetter is removed.Release impact
The storage change is additive. Every saved
data.jsongets a rootlibraryIdstring, and a file without one reads as"".schemaVersionstays 2, and no migration runs. Released 2.21.1 keeps the field when it saves.Known limits:
Note
Prevent pre-sync devices from wiping the synced library via library identifiers
libraryIdto every persisted data document. New installs get a timestamp+UUID identity; legacy files decode with an empty pre-identifier marker in podNotesData.tsmergeDataFilein merge.ts handles two documents with different library identifiers by merging both libraries and picking the lexicographically earlier identifier. Same-library merges use the ordinary three-way mergeDataFileWriter.persistandapplyExternalChangein writer.ts now usemergeDataFile. Failed writes are settled when their exact document is later observed on diskthreeWayMergeSettingsto recurse keys when the base object is absent, instead of taking the local value wholesalelibraryId(empty string for legacy)Macroscope summarized baee866.