From 0bfd8a6df42f4beca2b5106ce3b8ffef558bb10f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 21:50:06 +0000 Subject: [PATCH] Add diff cache size setting --- .../sync-client/src/persistence/settings.ts | 4 ++- frontend/sync-client/src/sync-client.ts | 22 +++++++++--- .../sync-client/src/sync-operations/syncer.ts | 4 +-- .../src/utils/fix-sized-cache.test.ts | 36 +++++++++++++++++++ .../sync-client/src/utils/fix-sized-cache.ts | 28 +++++++++------ 5 files changed, 75 insertions(+), 19 deletions(-) diff --git a/frontend/sync-client/src/persistence/settings.ts b/frontend/sync-client/src/persistence/settings.ts index b0aff937..bd895d15 100644 --- a/frontend/sync-client/src/persistence/settings.ts +++ b/frontend/sync-client/src/persistence/settings.ts @@ -9,6 +9,7 @@ export interface SyncSettings { maxFileSizeMB: number; ignorePatterns: string[]; webSocketRetryIntervalMs: number; + diffCacheSizeMB: number; } export const DEFAULT_SETTINGS: SyncSettings = { @@ -19,7 +20,8 @@ export const DEFAULT_SETTINGS: SyncSettings = { isSyncEnabled: false, maxFileSizeMB: 10, ignorePatterns: [], - webSocketRetryIntervalMs: 3500 + webSocketRetryIntervalMs: 3500, + diffCacheSizeMB: 4 }; export class Settings { diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts index 33a1cac5..2bfc2654 100644 --- a/frontend/sync-client/src/sync-client.ts +++ b/frontend/sync-client/src/sync-client.ts @@ -38,7 +38,8 @@ export class SyncClient { private readonly _logger: Logger, private readonly connectionStatus: ConnectionStatus, private readonly cursorTracker: CursorTracker, - private readonly fileChangeNotifier: FileChangeNotifier + private readonly fileChangeNotifier: FileChangeNotifier, + private readonly contentCache: FixedSizeDocumentCache ) { this.settings.addOnSettingsChangeListener( async (newSettings, oldSettings) => { @@ -53,6 +54,14 @@ export class SyncClient { this.stop(); } } + + if ( + newSettings.diffCacheSizeMB !== oldSettings.diffCacheSizeMB + ) { + this.contentCache.resize( + newSettings.diffCacheSizeMB * 1024 * 1024 + ); + } } ); } @@ -65,6 +74,10 @@ export class SyncClient { return this.database.length; } + public get isWebSocketConnected(): boolean { + return this.webSocketManager.isWebSocketConnected; + } + public static async create({ fs, persistence, @@ -152,8 +165,7 @@ export class SyncClient { settings, syncService, fileOperations, - unrestrictedSyncer, - contentCache + unrestrictedSyncer ); const webSocketManager = new WebSocketManager( @@ -182,7 +194,8 @@ export class SyncClient { logger, connectionStatus, cursorTracker, - fileChangeNotifier + fileChangeNotifier, + contentCache ); logger.info("SyncClient initialised"); @@ -235,6 +248,7 @@ export class SyncClient { public async reset(): Promise { this.stop(); this.connectionStatus.startReset(); + this.contentCache.clear(); await this.syncer.reset(); this.history.reset(); this.database.reset(); diff --git a/frontend/sync-client/src/sync-operations/syncer.ts b/frontend/sync-client/src/sync-operations/syncer.ts index 1c8ac36e..a4badd9a 100644 --- a/frontend/sync-client/src/sync-operations/syncer.ts +++ b/frontend/sync-client/src/sync-operations/syncer.ts @@ -34,8 +34,7 @@ export class Syncer { settings: Settings, private readonly syncService: SyncService, private readonly operations: FileOperations, - private readonly internalSyncer: UnrestrictedSyncer, - private readonly contentCache: FixedSizeDocumentCache + private readonly internalSyncer: UnrestrictedSyncer ) { this.syncQueue = new PQueue({ concurrency: settings.getSettings().syncConcurrency @@ -252,7 +251,6 @@ export class Syncer { public async reset(): Promise { await this.waitUntilFinished(); - this.contentCache.clear(); } public async syncRemotelyUpdatedFile( diff --git a/frontend/sync-client/src/utils/fix-sized-cache.test.ts b/frontend/sync-client/src/utils/fix-sized-cache.test.ts index 46bc4144..4a24aafb 100644 --- a/frontend/sync-client/src/utils/fix-sized-cache.test.ts +++ b/frontend/sync-client/src/utils/fix-sized-cache.test.ts @@ -236,4 +236,40 @@ describe("fixedSizeDocumentCache", () => { assert.equal(cache.get(2), doc2); assert.equal(cache.get(3), doc3); }); + + it("resizeToLargerSizeNoEviction", async () => { + const cache = new FixedSizeDocumentCache(4); + const doc1 = new Uint8Array([1, 2]); + const doc2 = new Uint8Array([3, 4]); + + cache.put(1, doc1); + cache.put(2, doc2); + + cache.resize(10); + + assert.equal(cache.get(1), doc1); + assert.equal(cache.get(2), doc2); + }); + + it("resizeCausesMultipleEvictions", async () => { + const cache = new FixedSizeDocumentCache(10); + const doc1 = new Uint8Array([1, 2]); + const doc2 = new Uint8Array([3, 4]); + const doc3 = new Uint8Array([5, 6]); + const doc4 = new Uint8Array([7, 8]); + + cache.put(1, doc1); + cache.put(2, doc2); + cache.put(3, doc3); + cache.put(4, doc4); + // Cache has 8 bytes total + + cache.resize(2); + + // Should evict doc1, doc2, doc3 to get down to 2 bytes + assert.equal(cache.get(1), undefined); + assert.equal(cache.get(2), undefined); + assert.equal(cache.get(3), undefined); + assert.equal(cache.get(4), doc4); + }); }); diff --git a/frontend/sync-client/src/utils/fix-sized-cache.ts b/frontend/sync-client/src/utils/fix-sized-cache.ts index 7adee7b0..cf0ba47e 100644 --- a/frontend/sync-client/src/utils/fix-sized-cache.ts +++ b/frontend/sync-client/src/utils/fix-sized-cache.ts @@ -14,14 +14,12 @@ class LRUNode { // evicting the least recently used documents when the size limit is exceeded. export class FixedSizeDocumentCache { - private readonly maxSizeInBytes: number; private currentSizeInBytes: number; private readonly cache: Map; private head: LRUNode | null; // Least recently used private tail: LRUNode | null; // Most recently used - public constructor(maxSizeInBytes: number) { - this.maxSizeInBytes = maxSizeInBytes; + public constructor(private maxSizeInBytes: number) { this.currentSizeInBytes = 0; this.cache = new Map(); this.head = null; @@ -56,14 +54,7 @@ export class FixedSizeDocumentCache { this.cache.set(updateId, newNode); this.addToTail(newNode); this.currentSizeInBytes += content.byteLength; - - // Evict least recently used documents if over size limit - while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) { - const lruNode = this.head; - this.removeNode(lruNode); - this.cache.delete(lruNode.key); - this.currentSizeInBytes -= lruNode.value.byteLength; - } + this.fitBelowMaxSize(); } public clear(): void { @@ -73,6 +64,21 @@ export class FixedSizeDocumentCache { this.currentSizeInBytes = 0; } + public resize(newMaxSizeInBytes: number): void { + this.maxSizeInBytes = newMaxSizeInBytes; + this.fitBelowMaxSize(); + } + + private fitBelowMaxSize(): void { + // Evict least recently used documents if over size limit + while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) { + const lruNode = this.head; + this.removeNode(lruNode); + this.cache.delete(lruNode.key); + this.currentSizeInBytes -= lruNode.value.byteLength; + } + } + private removeNode(node: LRUNode): void { if (node.prev) { node.prev.next = node.next;