From ade39de8a02dfd86bb56e6cec3b948ddcd62279d Mon Sep 17 00:00:00 2001 From: HeroBlackInk Date: Wed, 25 Feb 2026 16:02:24 +0800 Subject: [PATCH] fix(sync): serialize rebuild cache and avoid transient empty mapping Acquire an exclusive sync lock during rebuildCache and release it in finally, while removing the intermediate taskFileMapping clear step that could be persisted by debounced saves. This keeps rebuild atomic from reader and persistence perspectives. --- src/data/cache.ts | 22 +++++++++++++++------- src/sync/syncLock.ts | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/data/cache.ts b/src/data/cache.ts index f65edf0..85083a5 100644 --- a/src/data/cache.ts +++ b/src/data/cache.ts @@ -709,7 +709,18 @@ export class CacheOperation { */ async rebuildCache(noticeCallback?: (message: string) => void): Promise { const backupMapping = { ...this.plugin.settings.taskFileMapping }; + let exclusiveLockAcquired = false; try { + const syncLockManager = this.plugin.syncLockManager; + if (!syncLockManager) { + throw new Error('Sync lock manager is not initialized'); + } + + exclusiveLockAcquired = await syncLockManager.acquireExclusive(); + if (!exclusiveLockAcquired) { + throw new Error('Unable to acquire sync lock for cache rebuild. Please retry in a few seconds.'); + } + if (noticeCallback) { noticeCallback('Starting cache rebuild...'); } else { @@ -718,18 +729,11 @@ export class CacheOperation { this.plugin.logOperation?.log('CACHE_REBUILT', 'Starting cache rebuild...'); // ========================================================================================== - // Step 1: 清空 taskFileMapping // ========================================================================================== // // 【说明】 - // taskFileMapping 是从任务 ID 到文件位置的映射 - // 重建缓存时需要重新扫描,所以先清空 - // fileMetadata 保留,因为文件本身可能还在 // // ========================================================================================== - - // Step 1: Backup old mapping, then clear (restore on failure) - await this.plugin.safeSettings?.update({ taskFileMapping: {} }, false); // ========================================================================================== // Step 2: 确保 syncData 已加载 @@ -1118,6 +1122,10 @@ export class CacheOperation { new Notice(message); } return { success: false, tasksProcessed: 0, conflictsCount: 0, nonActiveCount: 0, issueCount: 0, convertedCount: 0, tasksWithoutIdCount: 0 }; + } finally { + if (exclusiveLockAcquired) { + this.plugin.syncLockManager?.release(); + } } } diff --git a/src/sync/syncLock.ts b/src/sync/syncLock.ts index 317b9f3..60c1fc8 100644 --- a/src/sync/syncLock.ts +++ b/src/sync/syncLock.ts @@ -37,6 +37,27 @@ export class SyncLockManager { return !this.plugin.saveLock; } + async acquireExclusive(): Promise { + if (this.plugin.saveLock) { + this.plugin.debugLog('save locked, waiting before exclusive sync acquire.'); + const saveReleased = await this.waitForSaveRelease(); + if (!saveReleased) { + this.plugin.debugLog('save lock did not release in time.'); + return false; + } + } + + if (this.locked) { + this.plugin.debugLog('sync locked. waiting for exclusive acquire.'); + const released = await this.waitForRelease(); + if (!released) return false; + this.plugin.debugLog('sync unlocked.'); + } + + this.locked = true; + return true; + } + async acquire(direction?: SyncDirection): Promise { if (!this.plugin.settings.syncEnabled) {