lib_store.js

import EventEmitter from 'events'

/**
 * Cache store class
 *
 * @class
 */
class Store extends EventEmitter {
  /**
   * Constructor.
   *
   * @param {Bajo.Plugin} plugin
   */
  constructor (plugin) {
    super()
    /**
     * Reference to the plugin instance.
     * @type {Bajo.Plugin}
     */
    this.plugin = plugin
    /**
     * Reference to the app instance.
     * @type {Bajo.App}
     */
    this.app = plugin.app
    /**
     * Reference to the storage model.
     * @type {Dobo.Model}
     */
    this.storage = this.app.dobo.getModel('CacheStorage')
  }

  /**
   * Utility function to build options for storage operations.
   *
   * @param {Object} items - Items to build options for
   * @returns {Object} - Built options
   */
  buildOpts = (items = {}) => {
    const { defaultsDeep } = this.app.lib.aneka
    const def = { noMagic: true, noResult: true }
    return defaultsDeep(items, def)
  }

  _getContent = async (key, result) => {
    const { get } = this.app.lib._
    let content = get(result, 'content')
    if (!content) return undefined
    content = JSON.parse(content)
    if (content.expires && Date.now() > content.expires) {
      await this.delete(key)
      return undefined
    }
    return JSON.stringify(content)
  }

  /**
   * Get the cached item for the given key.
   *
   * @async
   * @method
   * @param {string} key - The key of the cached item
   * @returns {Promise<any>} - Returns the cached item or undefined if expired/not found
   */
  get = async (key) => {
    const result = await this.storage.getRecord(key, this.buildOpts())
    return await this._getContent(key, result)
  }

  /**
   * Get multiple cached items for the given keys.
   *
   * @async
   * @method
   * @param {string[]} keys - The keys of the cached items
   * @returns {Promise<any[]>} - Returns an array of cached items or undefined for expired/not found items
   */
  getMany = async (keys = []) => {
    const filter = {
      query: { id: { $in: keys } }
    }
    const results = await this.storage.findAllRecord(filter, this.buildOpts())
    const values = []
    for (const k of keys) {
      const result = results.find(r => r.key === k)
      if (!result) {
        values.push(undefined)
        continue
      }
      const value = await this._getContent(k, result)
      values.push(value)
    }
    return values
  }

  /**
   * Set a cached item for the given key with an optional TTL.
   *
   * @async
   * @method
   * @param {string} key - The key of the cached item
   * @param {any} value - The value to cache
   * @param {number} ttl - Time to live in milliseconds
   * @returns {Promise<boolean>} - Returns true if the item was successfully cached
   */
  set = async (key, value, ttl = 0) => {
    const exp = Date.now() + ttl
    let content = value
    try {
      content = JSON.parse(value)
      content.expires = exp
      content = JSON.stringify(content)
    } catch (err) {}
    const body = { id: key, content, exp }
    try {
      const [, model, action] = key.split('|')
      body.model = model
      body.action = action
    } catch (err) {}
    await this.storage.upsertRecord(body, this.buildOpts())
    return true
  }

  /**
   * Delete the cached item for the given key.
   *
   * @async
   * @method
   * @param {string} key - The key of the cached item
   * @returns {Promise<boolean>} - Returns true if the item was successfully deleted
   */
  delete = async (key) => {
    try {
      await this.storage.removeRecord(key, this.buildOpts())
    } catch (err) {
      this.plugin.log.error('errDelCache%s', err.message)
    }
    return true
  }

  /**
   * Delete multiple cached items for the given keys.
   *
   * @async
   * @method
   * @param {string[]} keys - The keys of the cached items
   * @returns {Promise<boolean>} - Returns true if the items were successfully deleted
   */
  deleteMany = async (keys = []) => {
    for (const k of keys) {
      await this.delete(k)
    }
    return true
  }

  /**
   * Clear all cached items.
   *
   * @async
   * @method
   * @returns {Promise<boolean>} - Returns true if the cache was successfully cleared
   */
  clear = async () => {
    try {
      await this.storage.clear()
    } catch (err) {
      this.plugin.log.error('errClearCache%s', err.message)
    }
    return true
  }

  /**
   * Clear all expired cached items.
   *
   * @async
   * @method
   * @returns {Promise<boolean>} - Returns true if the expired cache items were successfully cleared
   */
  clearExpired = async () => {
    const filter = {
      query: { exp: { $lte: Date.now() } }
    }
    const items = await this.model.findAllRecord(filter, this.buildOpts())
    for (const id of items.map(i => i.id)) {
      try {
        await this.model.removeRecord(id, this.buildOpts())
      } catch (err) {
        this.plugin.log.error('errClearExpCache%s', err.message)
      }
    }
    return true
  }

  /**
   * Check if a cached item exists for the given key.
   *
   * @async
   * @method
   * @param {string} key - The key of the cached item
   * @returns {Promise<boolean>} - Returns true if the item exists and is not expired
   */
  has = async (key) => {
    const filter = {
      query: { id: key }
    }
    const result = await this.model.findOneRecord(filter, this.buildOpts())
    return await this._getContent(key, result)
  }

  /*
  async * iterator () {
    let page = 0
    for (;;) {
      const data = await this.storage.findRecord({ page, limit: 20 }, { noHook: true, noModelHook: true, noDynHook: true, dataOnly: true })
      if (data.length === 0) break
      page++
      for (const d of data) {
        yield [d.id, d.content]
      }
    }
  }
  */
}

export default Store