JavaScript 具有大小限制和最近最少使用 (LRU) 逐出的本地存储缓存

JavaScript localStorage cache with size limit and least-recently used (LRU) eviction

我正在寻找一种在浏览器中执行 Memcached 提供的方法,即配置大小限制(例如 localStorage 配额)的能力,它会自动驱逐旧项目以将缓存保持在限制范围内。

好处是不需要显式删除,只要缓存键是 versioned/timestamped。我已经看到一些库与 "number of items" 限制类似,但大小限制对于保持在浏览器的配额以下会更有用。

这无法完美实现,因为无法保证浏览器如何存储本地存储的内容,但可以创建足够接近的实现。

我们可以从JS中的字符串是一个16-bit unsigned integer value开始(因为在localStorage中所有的东西都是以字符串形式存储的)。这意味着我们可以使用 content.length * 16 / 8.

轻松获得任何字符串的大小(以字节为单位)

所以现在我们只需要创建一个缓存来跟踪存储内容的大小和键的大小,它将内容存储在下面。

一个非常原始和幼稚的实现可能是:

class Cache {

  private keyStoreKey: string;
  private cacheSizeKey: string;

  /**
   * List of the keys stored in this cache.
   */
  private storedKeys: string[] = [];

  /**
   * The size of the stored values in bytes
   */
  private cacheSize: number = 0;

  constructor(
    private prefix: string, 
    private sizeLimit: number,
  ) {
    if(this.sizeLimit < 100) {
      // the minimal size of the cache is 24 + prefix, otherwise, it would enter 
      // a loop trying to remove every element on any insert operation.
      throw new Error('Cache size must be above 100 bytes');
    }

    this.keyStoreKey = `${prefix}:k`;
    this.cacheSizeKey = `${prefix}:s`;

    this.cacheSize = localStorage.getItem(this.cacheSizeKey) ? Number.parseInt(localStorage.getItem(this.cacheSizeKey)) : 0;
    this.storedKeys = localStorage.getItem(this.keyStoreKey) ? localStorage.getItem(this.keyStoreKey).split(',') : [];
  }

  /**
   * The size of the keys in bytes
   */
  public get keyStoreSize() {
    return this.calculateLenght(this.storedKeys.join(`${this.prefix}:v:,`));
  }

  public get totalUsedSize() {
    return this.cacheSize + this.keyStoreSize + this.calculateLenght(this.keyStoreKey) + this.calculateLenght(this.cacheSizeKey);
  }

  /**
   * Returns the size of the given string in bytes.
   * 
   * The ECMAScript specification defines character as single 16-bit unit of UTF-16 text
   */
  private calculateLenght(content: string): number {
    return content.length * 16 / 8;
  }

  /**
   * Saves an item into the cahce.
   * 
   * NOTE: name cannot contain commas.
   */
  public set(name: string, content: string) {
    const newContentSize = this.calculateLenght(content);

    if(!(this.storedKeys).some(storedName => storedName === name)) {
      this.storedKeys.unshift(name);

      this.cacheSize += newContentSize;
    } else {
      this.storedKeys = this.storedKeys.filter(n => n !== name);
      this.storedKeys.unshift(name);      

      const oldContentSize = this.calculateLenght(localStorage.getItem(`${this.prefix}:v:${name}`));

      this.cacheSize = this.cacheSize - oldContentSize + newContentSize;
    }

    while(this.totalUsedSize > this.sizeLimit && this.storedKeys.length > 0) {
      this.removeOldestItem();
    }

    localStorage.setItem(this.cacheSizeKey, this.cacheSize.toString());  
    localStorage.setItem(`${this.prefix}:debug:totalSize`, this.totalUsedSize.toString());  
    localStorage.setItem(this.keyStoreKey, this.storedKeys.join(','));
    localStorage.setItem(`${this.prefix}:v:${name}`, content);
  }

  /**
   * The oldest item is the last in the stored keys array
   */
  private removeOldestItem() {
    const name = this.storedKeys.pop();
    const oldContentSize = this.calculateLenght(localStorage.getItem(`${this.prefix}:v:${name}`));

    this.cacheSize -= oldContentSize;

    localStorage.removeItem(`${this.prefix}:v:${name}`);
  }
}

window['cache'] = new Cache('test', 200);

我没有实现读取数据的函数,但是因为键存储在数组中,所以您可以轻松实现 getMostRecent() 或 getNthRecent(position) 或只是一个简单的 get(key) 函数。

实现在 Typescript 中,如果您不熟悉它,请忽略未知部分。

如果您想为该功能使用一个已经存在的解决方案,那么您可以查看这个库 runtime-memcache 实现 lru 和一些其他缓存方案(mrutimeout) 在 javascript.

它使用修改后的双向链表实现 getsetremove 的 O(1)。