{"version":3,"file":"SingleContextStorageLruCache.mjs","names":[],"sources":["../../../../src/modules/cache/singleContextLruCache/SingleContextStorageLruCache.ts"],"sourcesContent":["import LRUMap from 'lru_map'\nimport type { AgentContext } from '../../../agent/context'\nimport { CredoError, RecordDuplicateError } from '../../../error'\nimport type { Cache } from '../Cache'\nimport type { SingleContextLruCacheItem } from './SingleContextLruCacheRecord'\n\nimport { SingleContextLruCacheRecord } from './SingleContextLruCacheRecord'\nimport { SingleContextLruCacheRepository } from './SingleContextLruCacheRepository'\n\nconst CONTEXT_STORAGE_LRU_CACHE_ID = 'CONTEXT_STORAGE_LRU_CACHE_ID'\n\nexport interface SingleContextStorageLruCacheOptions {\n  /** The maximum number of entries allowed in the cache */\n  limit: number\n}\n\n/**\n * Cache that leverages the storage associated with the agent context to store cache records.\n * It will keep an in-memory cache of the records to avoid hitting the storage on every read request.\n * Therefor this cache is meant to be used with a single instance of the agent.\n *\n * Due to keeping an in-memory copy of the cache, it is also not meant to be used with multiple\n * agent context instances (meaning multi-tenancy), as they will overwrite the in-memory cache.\n *\n * However, this means the cache is not meant for usage with multiple instances.\n */\nexport class SingleContextStorageLruCache implements Cache {\n  private limit: number\n  private _cache?: LRUMap.LRUMap<string, SingleContextLruCacheItem>\n  private _contextCorrelationId?: string\n\n  public constructor({ limit }: SingleContextStorageLruCacheOptions) {\n    this.limit = limit\n  }\n\n  public async get<CacheValue>(agentContext: AgentContext, key: string) {\n    this.assertContextCorrelationId(agentContext)\n\n    const cache = await this.getCache(agentContext)\n    this.removeExpiredItems(cache)\n\n    const item = cache.get(key)\n\n    // Does not exist\n    if (!item) return null\n\n    // Expired\n    if (item.expiresAt && Date.now() > item.expiresAt) {\n      cache.delete(key)\n      await this.persistCache(agentContext)\n      return null\n    }\n\n    return item.value as CacheValue\n  }\n\n  public async set<CacheValue>(\n    agentContext: AgentContext,\n    key: string,\n    value: CacheValue,\n    expiresInSeconds?: number\n  ): Promise<void> {\n    this.assertContextCorrelationId(agentContext)\n\n    let expiresDate: Date | undefined\n\n    if (expiresInSeconds) {\n      expiresDate = new Date()\n      expiresDate.setSeconds(expiresDate.getSeconds() + expiresInSeconds)\n    }\n\n    const cache = await this.getCache(agentContext)\n    this.removeExpiredItems(cache)\n\n    cache.set(key, {\n      expiresAt: expiresDate?.getTime(),\n      value,\n    })\n    await this.persistCache(agentContext)\n  }\n\n  public async remove(agentContext: AgentContext, key: string): Promise<void> {\n    this.assertContextCorrelationId(agentContext)\n\n    const cache = await this.getCache(agentContext)\n    this.removeExpiredItems(cache)\n    cache.delete(key)\n\n    await this.persistCache(agentContext)\n  }\n\n  private async getCache(agentContext: AgentContext) {\n    if (!this._cache) {\n      const cacheRecord = await this.fetchCacheRecord(agentContext)\n      this._cache = this.lruFromRecord(cacheRecord)\n    }\n\n    return this._cache\n  }\n\n  private lruFromRecord(cacheRecord: SingleContextLruCacheRecord) {\n    return new LRUMap.LRUMap<string, SingleContextLruCacheItem>(this.limit, cacheRecord.entries.entries())\n  }\n\n  private async fetchCacheRecord(agentContext: AgentContext) {\n    const cacheRepository = agentContext.dependencyManager.resolve(SingleContextLruCacheRepository)\n    let cacheRecord = await cacheRepository.findById(agentContext, CONTEXT_STORAGE_LRU_CACHE_ID)\n\n    if (!cacheRecord) {\n      cacheRecord = new SingleContextLruCacheRecord({\n        id: CONTEXT_STORAGE_LRU_CACHE_ID,\n        entries: new Map(),\n      })\n\n      try {\n        await cacheRepository.save(agentContext, cacheRecord)\n      } catch (error) {\n        // This addresses some race conditions issues where we first check if the record exists\n        // then we create one if it doesn't, but another process has created one in the meantime\n        // Although not the most elegant solution, it addresses the issues\n        if (error instanceof RecordDuplicateError) {\n          // the record already exists, which is our intended end state\n          // we can ignore this error and fetch the existing record\n          return cacheRepository.getById(agentContext, CONTEXT_STORAGE_LRU_CACHE_ID)\n        }\n        throw error\n      }\n    }\n\n    return cacheRecord\n  }\n\n  private removeExpiredItems(cache: LRUMap.LRUMap<string, SingleContextLruCacheItem>) {\n    cache.forEach((value, key) => {\n      if (value.expiresAt && Date.now() > value.expiresAt) {\n        cache.delete(key)\n      }\n    })\n  }\n\n  private async persistCache(agentContext: AgentContext) {\n    const cacheRepository = agentContext.dependencyManager.resolve(SingleContextLruCacheRepository)\n    const cache = await this.getCache(agentContext)\n\n    await cacheRepository.update(\n      agentContext,\n      new SingleContextLruCacheRecord({\n        entries: new Map(cache.toJSON().map(({ key, value }) => [key, value])),\n        id: CONTEXT_STORAGE_LRU_CACHE_ID,\n      })\n    )\n  }\n\n  /**\n   * Asserts this class is not used with multiple agent context instances.\n   */\n  private assertContextCorrelationId(agentContext: AgentContext) {\n    if (!this._contextCorrelationId) {\n      this._contextCorrelationId = agentContext.contextCorrelationId\n    }\n\n    if (this._contextCorrelationId !== agentContext.contextCorrelationId) {\n      throw new CredoError(\n        'SingleContextStorageLruCache can not be used with multiple agent context instances. Register a custom cache implementation in the CacheModule.'\n      )\n    }\n  }\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,+BAA+B;;;;;;;;;;;AAiBrC,IAAa,+BAAb,MAA2D;CAKzD,AAAO,YAAY,EAAE,SAA8C;AACjE,OAAK,QAAQ;;CAGf,MAAa,IAAgB,cAA4B,KAAa;AACpE,OAAK,2BAA2B,aAAa;EAE7C,MAAM,QAAQ,MAAM,KAAK,SAAS,aAAa;AAC/C,OAAK,mBAAmB,MAAM;EAE9B,MAAM,OAAO,MAAM,IAAI,IAAI;AAG3B,MAAI,CAAC,KAAM,QAAO;AAGlB,MAAI,KAAK,aAAa,KAAK,KAAK,GAAG,KAAK,WAAW;AACjD,SAAM,OAAO,IAAI;AACjB,SAAM,KAAK,aAAa,aAAa;AACrC,UAAO;;AAGT,SAAO,KAAK;;CAGd,MAAa,IACX,cACA,KACA,OACA,kBACe;AACf,OAAK,2BAA2B,aAAa;EAE7C,IAAI;AAEJ,MAAI,kBAAkB;AACpB,iCAAc,IAAI,MAAM;AACxB,eAAY,WAAW,YAAY,YAAY,GAAG,iBAAiB;;EAGrE,MAAM,QAAQ,MAAM,KAAK,SAAS,aAAa;AAC/C,OAAK,mBAAmB,MAAM;AAE9B,QAAM,IAAI,KAAK;GACb,WAAW,aAAa,SAAS;GACjC;GACD,CAAC;AACF,QAAM,KAAK,aAAa,aAAa;;CAGvC,MAAa,OAAO,cAA4B,KAA4B;AAC1E,OAAK,2BAA2B,aAAa;EAE7C,MAAM,QAAQ,MAAM,KAAK,SAAS,aAAa;AAC/C,OAAK,mBAAmB,MAAM;AAC9B,QAAM,OAAO,IAAI;AAEjB,QAAM,KAAK,aAAa,aAAa;;CAGvC,MAAc,SAAS,cAA4B;AACjD,MAAI,CAAC,KAAK,QAAQ;GAChB,MAAM,cAAc,MAAM,KAAK,iBAAiB,aAAa;AAC7D,QAAK,SAAS,KAAK,cAAc,YAAY;;AAG/C,SAAO,KAAK;;CAGd,AAAQ,cAAc,aAA0C;AAC9D,SAAO,IAAI,OAAO,OAA0C,KAAK,OAAO,YAAY,QAAQ,SAAS,CAAC;;CAGxG,MAAc,iBAAiB,cAA4B;EACzD,MAAM,kBAAkB,aAAa,kBAAkB,QAAQ,gCAAgC;EAC/F,IAAI,cAAc,MAAM,gBAAgB,SAAS,cAAc,6BAA6B;AAE5F,MAAI,CAAC,aAAa;AAChB,iBAAc,IAAI,4BAA4B;IAC5C,IAAI;IACJ,yBAAS,IAAI,KAAK;IACnB,CAAC;AAEF,OAAI;AACF,UAAM,gBAAgB,KAAK,cAAc,YAAY;YAC9C,OAAO;AAId,QAAI,iBAAiB,qBAGnB,QAAO,gBAAgB,QAAQ,cAAc,6BAA6B;AAE5E,UAAM;;;AAIV,SAAO;;CAGT,AAAQ,mBAAmB,OAAyD;AAClF,QAAM,SAAS,OAAO,QAAQ;AAC5B,OAAI,MAAM,aAAa,KAAK,KAAK,GAAG,MAAM,UACxC,OAAM,OAAO,IAAI;IAEnB;;CAGJ,MAAc,aAAa,cAA4B;EACrD,MAAM,kBAAkB,aAAa,kBAAkB,QAAQ,gCAAgC;EAC/F,MAAM,QAAQ,MAAM,KAAK,SAAS,aAAa;AAE/C,QAAM,gBAAgB,OACpB,cACA,IAAI,4BAA4B;GAC9B,SAAS,IAAI,IAAI,MAAM,QAAQ,CAAC,KAAK,EAAE,KAAK,YAAY,CAAC,KAAK,MAAM,CAAC,CAAC;GACtE,IAAI;GACL,CAAC,CACH;;;;;CAMH,AAAQ,2BAA2B,cAA4B;AAC7D,MAAI,CAAC,KAAK,sBACR,MAAK,wBAAwB,aAAa;AAG5C,MAAI,KAAK,0BAA0B,aAAa,qBAC9C,OAAM,IAAI,WACR,iJACD"}