Fixed: Hidden file JSON conflicts no longer keep re-opening and dismissing the merge dialogue before we can act, which fixes persistent unresolvable data.json conflicts in plug-in settings sync (related: #850).

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
vorotamoroz
2026-04-25 17:22:25 +09:00
co-authored by Copilot
parent b5d054f259
commit 31bd270869
3 changed files with 48 additions and 16 deletions
@@ -30,7 +30,8 @@
type JSONData = Record<string | number | symbol, any> | [any]; type JSONData = Record<string | number | symbol, any> | [any];
const docsArray = $derived.by(() => { const docsArray = $derived.by(() => {
if (docs && docs.length >= 1) { // The merge pane compares two revisions, so guard against incomplete input before reading docs[1].
if (docs && docs.length >= 2) {
if (keepOrder || docs[0].mtime < docs[1].mtime) { if (keepOrder || docs[0].mtime < docs[1].mtime) {
return { a: docs[0], b: docs[1] } as const; return { a: docs[0], b: docs[1] } as const;
} else { } else {
@@ -636,10 +636,24 @@ Offline Changed files: ${processFiles.length}`;
// --> Conflict processing // --> Conflict processing
// Keep one in-flight conflict check per path so repeated sync events do not close the active merge dialogue.
pendingConflictChecks = new Set<FilePathWithPrefix>();
queueConflictCheck(path: FilePathWithPrefix) { queueConflictCheck(path: FilePathWithPrefix) {
if (this.pendingConflictChecks.has(path)) return;
this.pendingConflictChecks.add(path);
this.conflictResolutionProcessor.enqueue(path); this.conflictResolutionProcessor.enqueue(path);
} }
finishConflictCheck(path: FilePathWithPrefix) {
this.pendingConflictChecks.delete(path);
}
requeueConflictCheck(path: FilePathWithPrefix) {
this.finishConflictCheck(path);
this.queueConflictCheck(path);
}
async resolveConflictOnInternalFiles() { async resolveConflictOnInternalFiles() {
// Scan all conflicted internal files // Scan all conflicted internal files
const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true }); const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true });
@@ -648,7 +662,7 @@ Offline Changed files: ${processFiles.length}`;
for await (const doc of conflicted) { for await (const doc of conflicted) {
if (!("_conflicts" in doc)) continue; if (!("_conflicts" in doc)) continue;
if (isInternalMetadata(doc._id)) { if (isInternalMetadata(doc._id)) {
this.conflictResolutionProcessor.enqueue(doc.path); this.queueConflictCheck(doc.path);
} }
} }
} catch (ex) { } catch (ex) {
@@ -679,21 +693,27 @@ Offline Changed files: ${processFiles.length}`;
const cc = await this.localDatabase.getRaw(id, { conflicts: true }); const cc = await this.localDatabase.getRaw(id, { conflicts: true });
if (cc._conflicts?.length === 0) { if (cc._conflicts?.length === 0) {
await this.extractInternalFileFromDatabase(stripAllPrefixes(path)); await this.extractInternalFileFromDatabase(stripAllPrefixes(path));
this.finishConflictCheck(path);
} else { } else {
this.conflictResolutionProcessor.enqueue(path); this.requeueConflictCheck(path);
} }
// check the file again // check the file again
} }
conflictResolutionProcessor = new QueueProcessor( conflictResolutionProcessor = new QueueProcessor(
async (paths: FilePathWithPrefix[]) => { async (paths: FilePathWithPrefix[]) => {
const path = paths[0]; const path = paths[0];
sendSignal(`cancel-internal-conflict:${path}`);
try { try {
// Retrieve data // Retrieve data
const id = await this.path2id(path, ICHeader); const id = await this.path2id(path, ICHeader);
const doc = await this.localDatabase.getRaw<MetaEntry>(id, { conflicts: true }); const doc = await this.localDatabase.getRaw<MetaEntry>(id, { conflicts: true });
if (doc._conflicts === undefined) return []; if (doc._conflicts === undefined) {
if (doc._conflicts.length == 0) return []; this.finishConflictCheck(path);
return [];
}
if (doc._conflicts.length == 0) {
this.finishConflictCheck(path);
return [];
}
this._log(`Hidden file conflicted:${path}`); this._log(`Hidden file conflicted:${path}`);
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0])); const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
const revA = doc._rev; const revA = doc._rev;
@@ -725,7 +745,7 @@ Offline Changed files: ${processFiles.length}`;
await this.storeInternalFileToDatabase({ path: filename, ...stat }); await this.storeInternalFileToDatabase({ path: filename, ...stat });
await this.extractInternalFileFromDatabase(filename); await this.extractInternalFileFromDatabase(filename);
await this.localDatabase.removeRevision(id, revB); await this.localDatabase.removeRevision(id, revB);
this.conflictResolutionProcessor.enqueue(path); this.requeueConflictCheck(path);
return []; return [];
} else { } else {
this._log(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE); this._log(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
@@ -743,6 +763,7 @@ Offline Changed files: ${processFiles.length}`;
await this.resolveByNewerEntry(id, path, doc, revA, revB); await this.resolveByNewerEntry(id, path, doc, revA, revB);
return []; return [];
} catch (ex) { } catch (ex) {
this.finishConflictCheck(path);
this._log(`Failed to resolve conflict (Hidden): ${path}`); this._log(`Failed to resolve conflict (Hidden): ${path}`);
this._log(ex, LOG_LEVEL_VERBOSE); this._log(ex, LOG_LEVEL_VERBOSE);
return []; return [];
@@ -761,15 +782,22 @@ Offline Changed files: ${processFiles.length}`;
const prefixedPath = addPrefix(path, ICHeader); const prefixedPath = addPrefix(path, ICHeader);
const docAMerge = await this.localDatabase.getDBEntry(prefixedPath, { rev: revA }); const docAMerge = await this.localDatabase.getDBEntry(prefixedPath, { rev: revA });
const docBMerge = await this.localDatabase.getDBEntry(prefixedPath, { rev: revB }); const docBMerge = await this.localDatabase.getDBEntry(prefixedPath, { rev: revB });
if (docAMerge != false && docBMerge != false) { try {
if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) { if (docAMerge != false && docBMerge != false) {
// Again for other conflicted revisions. if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) {
this.conflictResolutionProcessor.enqueue(path); // Again for other conflicted revisions.
this.requeueConflictCheck(path);
} else {
this.finishConflictCheck(path);
}
return;
} else {
// If either revision could not read, force resolving by the newer one.
await this.resolveByNewerEntry(id, path, doc, revA, revB);
} }
return; } catch (ex) {
} else { this.finishConflictCheck(path);
// If either revision could not read, force resolving by the newer one. throw ex;
await this.resolveByNewerEntry(id, path, doc, revA, revB);
} }
}, },
{ {
@@ -793,6 +821,8 @@ Offline Changed files: ${processFiles.length}`;
const storeFilePath = strippedPath; const storeFilePath = strippedPath;
const displayFilename = `${storeFilePath}`; const displayFilename = `${storeFilePath}`;
// const path = this.prefixedConfigDir2configDir(stripAllPrefixes(docA.path)) || docA.path; // const path = this.prefixedConfigDir2configDir(stripAllPrefixes(docA.path)) || docA.path;
// Cancel only when replacing an existing dialogue for the same path, not on every queue pass.
sendSignal(`cancel-internal-conflict:${docA.path}`);
const modal = new JsonResolveModal(this.app, storageFilePath, [docA, docB], async (keep, result) => { const modal = new JsonResolveModal(this.app, storageFilePath, [docA, docB], async (keep, result) => {
// modal.close(); // modal.close();
try { try {
@@ -1164,7 +1194,7 @@ Offline Changed files: ${files.length}`;
// Check if the file is conflicted, and if so, enqueue to resolve. // Check if the file is conflicted, and if so, enqueue to resolve.
// Until the conflict is resolved, the file will not be processed. // Until the conflict is resolved, the file will not be processed.
if (docMeta._conflicts && docMeta._conflicts.length > 0) { if (docMeta._conflicts && docMeta._conflicts.length > 0) {
this.conflictResolutionProcessor.enqueue(path); this.queueConflictCheck(path);
this._log(`${headerLine} Hidden file conflicted, enqueued to resolve`); this._log(`${headerLine} Hidden file conflicted, enqueued to resolve`);
return true; return true;
} }
+1
View File
@@ -12,6 +12,7 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
- Improved background worker crash cleanup so pending split/encryption tasks are released cleanly instead of being left in a waiting state (related: #855). - Improved background worker crash cleanup so pending split/encryption tasks are released cleanly instead of being left in a waiting state (related: #855).
- On start-up, the selected remote configuration is now applied to runtime connection fields as well, reducing intermittent authentication failures caused by stale runtime settings (related: #855). - On start-up, the selected remote configuration is now applied to runtime connection fields as well, reducing intermittent authentication failures caused by stale runtime settings (related: #855).
- Issue report generation now redacts `remoteConfigurations` connection strings and keeps only the scheme (e.g. `sls+https://`), so credentials are not exposed in reports. - Issue report generation now redacts `remoteConfigurations` connection strings and keeps only the scheme (e.g. `sls+https://`), so credentials are not exposed in reports.
- Hidden file JSON conflicts no longer keep re-opening and dismissing the merge dialogue before we can act, which fixes persistent unresolvable `data.json` conflicts in plug-in settings sync (related: #850).
## 0.25.57 ## 0.25.57