From fc2b35e0dc8538a639ba7efd8bcfd97001fc769d Mon Sep 17 00:00:00 2001 From: s-qin Date: Fri, 23 Oct 2026 23:54:13 +0800 Subject: [PATCH] perf(startup): inspect storage sizes without loading healthy files --- .../startupLifecycle/incompleteDocuments.ts | 33 ++++++++++++----- .../incompleteDocuments.unit.spec.ts | 36 +++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/serviceFeatures/startupLifecycle/incompleteDocuments.ts b/src/serviceFeatures/startupLifecycle/incompleteDocuments.ts index 9836d682..0b4ee3de 100644 --- a/src/serviceFeatures/startupLifecycle/incompleteDocuments.ts +++ b/src/serviceFeatures/startupLifecycle/incompleteDocuments.ts @@ -31,7 +31,7 @@ export interface IncompleteDocumentsDependencies { readonly localDatabase: Pick; readonly getPath: StartupPathReader; readonly isTargetFile: (path: string) => Promise; - readonly storageAccess: Pick; + readonly storageAccess: Pick; readonly fileHandler: Pick; readonly keyValueDB: Pick; readonly noticeGroups: NoticeGroups; @@ -94,16 +94,11 @@ export async function checkIncompleteDocuments( } const isConflicted = metaDoc?._conflicts && metaDoc._conflicts.length > 0; - let storageFileContent; - try { - storageFileContent = await dependencies.storageAccess.readHiddenFileBinary(path); - } catch (e) { - Logger(`Failed to read file ${path}: Possibly unprocessed or missing`); - Logger(e, LOG_LEVEL_VERBOSE); + const storageStat = await dependencies.storageAccess.statHidden(path); + if (!storageStat || storageStat.type !== "file") { continue; } - // const storageFileBlob = createBlob(storageFileContent); - const sizeOnStorage = storageFileContent.byteLength; + let sizeOnStorage = storageStat.size; const recordedSize = doc.size; const docBlob = readAsBlob(doc); const actualSize = docBlob.size; @@ -113,6 +108,15 @@ export async function checkIncompleteDocuments( sizeOnStorage !== recordedSize || isConflicted ) { + let storageFileContent; + try { + storageFileContent = await dependencies.storageAccess.readHiddenFileBinary(path); + } catch (e) { + Logger(`Failed to read file ${path}: Possibly unprocessed or missing`); + Logger(e, LOG_LEVEL_VERBOSE); + continue; + } + sizeOnStorage = storageFileContent.byteLength; const contentMatched = await isDocContentSame(doc.data, storageFileContent); errorFiles.push({ path, @@ -125,6 +129,17 @@ export async function checkIncompleteDocuments( Logger( `Size mismatch for ${path}: ${recordedSize} (DB Recorded) , ${actualSize} (DB Stored) , ${sizeOnStorage} (Storage Stored), ${contentMatched ? "Content Matched" : "Content Mismatched"} ${isConflicted ? "Conflicted" : "Not Conflicted"}` ); + } else { + const currentStat = await dependencies.storageAccess.statHidden(path); + if ( + !currentStat || + currentStat.type !== "file" || + currentStat.size !== storageStat.size || + currentStat.mtime !== storageStat.mtime || + currentStat.ctime !== storageStat.ctime + ) { + throw new Error("Storage changed during the incomplete document check"); + } } } if (errorFiles.length == 0) { diff --git a/src/serviceFeatures/startupLifecycle/incompleteDocuments.unit.spec.ts b/src/serviceFeatures/startupLifecycle/incompleteDocuments.unit.spec.ts index f40305c4..2f30f12b 100644 --- a/src/serviceFeatures/startupLifecycle/incompleteDocuments.unit.spec.ts +++ b/src/serviceFeatures/startupLifecycle/incompleteDocuments.unit.spec.ts @@ -99,6 +99,7 @@ function createDependencies(findAllNormalDocs: FindAllNormalDocs = noDocuments, isTargetFile: vi.fn(async () => true), storageAccess: { readHiddenFileBinary, + statHidden: vi.fn(async (path: string) => getFixture(path)?.stub.stat ?? null), getFileStub, }, fileHandler: { storeFileToDB }, @@ -157,6 +158,41 @@ describe("checkIncompleteDocuments", () => { expect(noticeGroups.finish).toHaveBeenCalledWith("startup-integrity-check"); }); + it("does not read healthy storage contents only to determine their size", async () => { + const healthy = documentFixture("healthy.md", { storageContent: "abc" }); + const fixture = createDependencies(documentsFrom([healthy]), [healthy]); + + await expect(checkIncompleteDocuments(fixture.dependencies)).resolves.toBe(true); + + expect(fixture.readHiddenFileBinary).not.toHaveBeenCalled(); + expect(fixture.askSelectStringDialogue).not.toHaveBeenCalled(); + expect(fixture.dependencies.keyValueDB.set).toHaveBeenCalledWith("checkIncompleteDocs", true); + }); + + it("does not mark a scan complete when storage changes during the size check", async () => { + const healthy = documentFixture("changing.md", { storageContent: "abc" }); + const fixture = createDependencies(documentsFrom([healthy]), [healthy]); + vi.mocked(fixture.dependencies.storageAccess.statHidden) + .mockResolvedValueOnce(healthy.stub.stat) + .mockResolvedValueOnce({ ...healthy.stub.stat, mtime: 3 }); + + await expect(checkIncompleteDocuments(fixture.dependencies)).rejects.toThrow("Storage changed"); + + expect(fixture.readHiddenFileBinary).not.toHaveBeenCalled(); + expect(fixture.dependencies.keyValueDB.set).not.toHaveBeenCalled(); + }); + + it("still reads conflicted storage for the existing repair dialogue", async () => { + const conflicted = documentFixture("conflicted.md", { storageContent: "abc", conflicts: ["2-other"] }); + const fixture = createDependencies(documentsFrom([conflicted]), [conflicted]); + fixture.askSelectStringDialogue.mockImplementation(selectButton(0)); + + await expect(checkIncompleteDocuments(fixture.dependencies)).resolves.toBe(true); + + expect(fixture.readHiddenFileBinary).toHaveBeenCalledWith("conflicted.md"); + expect(fixture.dependencies.keyValueDB.set).not.toHaveBeenCalled(); + }); + it("repairs a recoverable document when FIX is selected", async () => { const recoverable = documentFixture("recoverable.md"); const fixture = createDependencies(documentsFrom([recoverable]), [recoverable]);