diff --git a/packages/drivers/odsp-driver/src/odspCache.ts b/packages/drivers/odsp-driver/src/odspCache.ts index 3cec5807c650..f7945a0f9f27 100644 --- a/packages/drivers/odsp-driver/src/odspCache.ts +++ b/packages/drivers/odsp-driver/src/odspCache.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { IDisposable } from "@fluidframework/common-definitions"; import { PromiseCache } from "@fluidframework/common-utils"; import { IOdspResolvedUrl, @@ -58,15 +59,74 @@ class GarbageCollector { } } +class MapWithExpiration extends Map implements IDisposable { + public disposed: boolean = false; + private readonly expirationTimeouts = new Map>(); + + constructor( + private readonly expiryMs: number, + ) { + super(); + } + private scheduleExpiration(key: TKey) { + this.expirationTimeouts.set( + key, + setTimeout( + () => { this.delete(key); }, + this.expiryMs, + ), + ); + } + + private cancelExpiration(key: TKey) { + const timeout = this.expirationTimeouts.get(key); + if (timeout !== undefined) { + clearTimeout(timeout); + this.expirationTimeouts.delete(key); + } + } + + get(key: TKey): TValue | undefined { + return super.get(key); + } + + set(key: TKey, value: TValue): this { + // Sliding window expiration policy (on write) + this.cancelExpiration(key); + this.scheduleExpiration(key); + + return super.set(key, value); + } + + delete(key: TKey): boolean { + this.cancelExpiration(key); + return super.delete(key); + } + + dispose(_error?: Error): void { + if (this.disposed) { + return; + } + this.disposed = true; + Array.from(this).forEach(([key]) => this.cancelExpiration(key)); + } +} + /** * Default local-only implementation of IPersistedCache, * used if no persisted cache is provided by the host */ -export class LocalPersistentCache implements IPersistedCache { - private readonly cache = new Map(); - private readonly gc = new GarbageCollector((key) => this.cache.delete(key)); +export class LocalPersistentCache implements IPersistedCache, IDisposable { + public get disposed(): boolean { return this.cache.disposed; } + private readonly cache: MapWithExpiration; - public constructor(private readonly snapshotExpiryPolicy = 30 * 1000) {} + public constructor(snapshotExpiryPolicy = 30 * 1000) { + this.cache = new MapWithExpiration(snapshotExpiryPolicy); + } + + public dispose(error?: Error): void { + this.cache.dispose(error); + } async get(entry: ICacheEntry): Promise { const key = this.keyFromEntry(entry); @@ -77,10 +137,6 @@ export class LocalPersistentCache implements IPersistedCache { async put(entry: ICacheEntry, value: any) { const key = this.keyFromEntry(entry); this.cache.set(key, value); - - // Do not keep items too long in memory - this.gc.cancel(key); - this.gc.schedule(key, this.snapshotExpiryPolicy); } async removeEntries(file: IFileEntry): Promise {