diff --git a/common/spaces/disk_space_primitives.ts b/common/spaces/disk_space_primitives.ts index 313b98e2..a7618aae 100644 --- a/common/spaces/disk_space_primitives.ts +++ b/common/spaces/disk_space_primitives.ts @@ -16,6 +16,9 @@ const excludedFiles = ["data.db", "data.db-journal", "sync.json"]; export class DiskSpacePrimitives implements SpacePrimitives { rootPath: string; + fileListCache: FileMeta[] = []; + fileListCacheTime = 0; + fileListCacheUpdating: AbortController | null = null; constructor(rootPath: string) { this.rootPath = Deno.realPathSync(rootPath); @@ -92,6 +95,11 @@ export class DiskSpacePrimitives implements SpacePrimitives { } file.close(); + // Invalidate cache and trigger an update + this.fileListCache = []; + this.fileListCacheTime = 0; + this.updateCacheInBackground(); + // Fetch new metadata return this.getFileMeta(name); } catch (e) { @@ -121,11 +129,44 @@ export class DiskSpacePrimitives implements SpacePrimitives { async deleteFile(name: string): Promise { const localPath = this.filenameToPath(name); await Deno.remove(localPath); + + // Invalidate cache and trigger an update + this.fileListCache = []; + this.fileListCacheTime = 0; + this.updateCacheInBackground(); } async fetchFileList(): Promise { + // console.log("Fetching file list"); + const startTime = performance.now(); + + // If the file list cache is less than 60 seconds old, return it + if ( + this.fileListCache.length > 0 && + startTime - this.fileListCacheTime < 60000 + ) { + // Trigger a background sync, but return the cached list while the cache is being updated + this.updateCacheInBackground(); + return this.fileListCache; + } + + // Otherwise get the file list and wait for it + const allFiles: FileMeta[] = await this.getFileList(); + + const endTime = performance.now(); + console.info("Fetched uncached file list in", endTime - startTime, "ms"); + + this.fileListCache = allFiles; + this.fileListCacheTime = startTime; + + return allFiles; + } + + private async getFileList(): Promise { const allFiles: FileMeta[] = []; for await (const file of walkPreserveSymlinks(this.rootPath)) { + // Uncomment to simulate a slow-ish disk + // await new Promise((resolve) => setTimeout(resolve, 1)); const fullPath = file.path; try { const s = await Deno.stat(fullPath); @@ -149,9 +190,42 @@ export class DiskSpacePrimitives implements SpacePrimitives { } } } - return allFiles; } + + private updateCacheInBackground() { + if (this.fileListCacheUpdating) { + // Cancel the existing background update, so we never return stale data + this.fileListCacheUpdating.abort(); + } + + const abortController = new AbortController(); + this.fileListCacheUpdating = abortController; + + const updatePromise = this.getFileList().then((allFiles) => { + if (abortController.signal.aborted) return; + + this.fileListCache = allFiles; + this.fileListCacheTime = performance.now(); + console.info( + "Updated file list cache in background:", + allFiles.length, + "files found", + ); + }).catch((error) => { + if (abortController.signal.aborted) return; + + if (error.name !== "AbortError") { + console.error("Error updating file list cache in background:", error); + } + }).finally(() => { + if (this.fileListCacheUpdating === abortController) { + this.fileListCacheUpdating = null; + } + }); + + return updatePromise; + } } async function* walkPreserveSymlinks(