src/persistence.js

const USER_DATA_KEY_PREFIX = 'User';
const SERVER_DATA_KEY_PREFIX = 'Server';
const GLOBAL_DATA_KEY = 'Global';

function keyForUserId(userId) {
  return USER_DATA_KEY_PREFIX + userId;
}

function keyForServerId(serverId) {
  return SERVER_DATA_KEY_PREFIX + serverId;
}

/**
 * @callback Persistence~editFunction
 * @param {Object} data - The current data associated with the key. If there is none, an empty object {} is given. This data can be manipulated and then returned to persist it.
 * @returns {Object} The new data to associate with the key.
 */

/**
 * Read or write persistent data that is persisted even if the process is killed.
 * The persistence is a key-value store backed by [fpersist]{@link https://www.npmjs.com/package/fpersist}.
 * You can store values for any key, but there are convenience methods provided for storing data
 * attached to a particular user or server, or in a global store.
 * Persistence can be accessed via {@link Monochrome#getPersistence}.
 * For examples of using persistence to store and retrieve persistent data, see the
 * [demo addQuote command]{@link https://github.com/mistval/monochrome-demo/blob/master/commands/addquote.js}
 * and [demo getRandomQuote command]{@link https://github.com/mistval/monochrome-demo/blob/master/commands/getrandomquote.js}.
 * @hideconstructor
 */
class Persistence {
  constructor(defaultPrefixes, logger, storagePlugin) {
    this.storage = storagePlugin;
    this.defaultPrefixes_ = defaultPrefixes;
    this.prefixesForServerId_ = {};
    this.logger = logger.child({
      component: 'Monochrome::Persistence',
    });

    this.getGlobalData().then(data => {
      this.prefixesForServerId_ = data.prefixes || {};
    }).catch(err => {
      this.logger.error({
        event: 'ERROR LOADING PREFIXES',
        err,
      });
    });
  }

  /**
   * Delete the value associated with a specified key.
   * If the key doesn't exist, nothing happens and no error is thrown.
   * @param {string} key
   * @async 
   */
  deleteData(key) {
    return this.storage.deleteKey(key);
  }

  /**
   * Get the value associated with the specified key. If no such value exists,
   * an empty object {} is returned.
   * @param {string} key
   * @returns {Object} The value associated with the specified key.
   * @async
   * @example
const data = await persistence.getData('some_key');
console.log(JSON.stringify(data));
   */
  getData(key) {
    return this.storage.getValue(key, {});
  }

  /**
   * Get the data associated with a user. If no such data exists, an empty
   * object {} is returned.
   * @param {string} userId
   * @returns {Object} The value associated with the specified userId
   * @async
   * @example
const userId = '123456789';
const data = await persistence.getDataForUser(userId);
console.log(JSON.stringify(data));
   */
  getDataForUser(userId) {
    return this.getData(keyForUserId(userId));
  }

  /**
   * Get the data associated with a server. If no such data exists, an empty
   * object {} is returned.
   * @param {string} serverId
   * @returns {Object} The value associated with the specified serverId
   * @async
   * @example
const serverId = '123456789';
const data = await persistence.getDataForServer(serverId);
console.log(JSON.stringify(data));
   */
  getDataForServer(serverId) {
    return this.getData(keyForServerId(serverId));
  }

  /**
   * Get the global data. If no such data exists, an empty
   * object {} is returned.
   * @returns {Object} The global data.
   * @async
   * @example
const data = await persistence.getGlobalData();
console.log(JSON.stringify(data));
   */
  getGlobalData() {
    return this.getData(GLOBAL_DATA_KEY);
  }

  /**
   * Get the command prefixes associated with a server ID. This method is synchronous, in order to avoid the overhead
   * of using promises. If called very soon after the bot starts, it might not return the correct prefixes. It
   * might return the default prefixes even though the server has custom prefixes.
   * @param {string} serverId
   * @returns {string[]}
   * @example
const serverId = '123456789';
const prefixes = persistence.getPrefixesForServer(serverId);
const firstPrefix = prefixes[0];
const numberOfPrefixes = prefixes.length;
   */
  getPrefixesForServer(serverId) {
    return this.prefixesForServerId_[serverId] || this.defaultPrefixes_;
  }

  /**
   * Get the primary prefix for the location where msg was sent. This method is synchronous, in order to avoid the overhead
   * of using promises. If called very soon after the bot starts, it might not return the correct prefixes. It
   * might return the default prefixes even though the server has custom prefixes.
   * @param {external:"Eris.Message"} msg
   * @returns {string}
   */
  getPrimaryPrefixForMessage(msg) {
    return this.getPrefixesForMessage(msg)[0];
  }

  /**
   * Get the prefixes for the location where msg was sent. This method is synchronous, in order to avoid the overhead
   * of using promises. If called very soon after the bot starts, it might not return the correct prefixes. It
   * might return the default prefixes even though the server has custom prefixes.
   * @param {external:"Eris.Message"} msg
   * @returns {string[]}
   */
  getPrefixesForMessage(msg) {
    return this.getPrefixesForServer(msg.channel.guild ? msg.channel.guild.id : msg.channel.id);
  }

  /**
   * Edit the data associated with a key. This function is atomic in the sense
   * that no one else can be editing the value for the same key at the same time.
   * @param {string} key
   * @param {Persistence~editFunction} editFunction - The function that performs the edit.
   * @async
   * @example
await persistence.editData('some_key', (data) => {
  data.randomNumber = Math.random() * 100;

  if (!data.numberOfTimesEdited) {
    data.numberOfTimesEdited = 0;
  }

  data.numberOfTimesEdited += 1;

  return data;
});
   */
  editData(key, editFunction) {
    return this.storage.editValue(key, editFunction, {});
  }

  /**
   * Edit the data associated with a user. This function is atomic in the sense
   * that no one else can be editing the value for the same key at the same time.
   * @param {string} userId
   * @param {Persistence~editFunction} editFunction - The function that performs the edit.
   * @async
   * @example
const userId = '123456789';
await editDataForUser(userId, (data) => {
  data.userIsWorthMyTime = false;
  return data;
});
   */
  editDataForUser(userId, editFunction) {
    let key = keyForUserId(userId);
    return this.editData(key, editFunction);
  }

  /**
   * Edit the data associated with a server. This function is atomic in the sense
   * that no one else can be editing the value for the same key at the same time.
   * @param {string} serverId
   * @param {Persistence~editFunction} editFunction - The function that performs the edit.
   * @async
   * @example
const serverId = '123456789';
await editDataForServer(serverId, (data) => {
  data.favoriteUserInServer = 'nobody';
  return data;
});
   */
  editDataForServer(serverId, editFunction) {
    let key = keyForServerId(serverId);
    return this.editData(key, editFunction);
  }

  /**
   * Edit the data global data. This function is atomic in the sense
   * that no one else can be editing the value for the same key at the same time.
   * @param {Persistence~editFunction} editFunction - The function that performs the edit.
   * @async
   * @example await editGlobalData((data) => {
  if (!data.scoreboard) {
    data.scoreboard = {};
  }

  data.scoreboard['John Wick'] = 100;
  return data;
});
   */
  editGlobalData(editFunction) {
    return this.editData(GLOBAL_DATA_KEY, editFunction);
  }

  /**
   * Edit the prefixes associated with a server.
   * @param {string} serverId
   * @param {string[]} prefixes - The new prefixes for the server.
   * @async
   * @example
const serverId = '123456789';
const newPrefixes = ['!', '@', '&!'];
await editPrefixesForServer(serverId, newPrefixes);
   */
  editPrefixesForServerId(serverId, prefixes) {
    if (!prefixes) {
      delete this.prefixesForServerId_[serverId];
    } else {
      const lowercasePrefixes = prefixes.map(prefix => prefix.toLowerCase());
      const uniqueLowercasePrefixes = lowercasePrefixes.filter((prefix, i) => lowercasePrefixes.indexOf(prefix) === i);
      this.prefixesForServerId_[serverId] = uniqueLowercasePrefixes;
    }

    return this.editData(GLOBAL_DATA_KEY, data => {
      data.prefixes = data.prefixes || {};
      data.prefixes[serverId] = this.prefixesForServerId_[serverId];
      return data;
    });
  }

  /**
   * Reset the prefixes associated with a server.
   * @param {string} serverId
   * @async
   */
  resetPrefixesForServerId(serverId) {
    return this.editPrefixesForServerId(serverId, undefined);
  }

  /**
   * Tell persistence that it should stop allowing write operations.
   * @async
   */
  stop() {
    return this.storage.close();
  }
}

module.exports = Persistence;