index.js

const Package = require("../package.json");
const { EventEmitter } = require("events");
const RESTManager = require("./rest/RESTManager");
const UserStore = require("./storage/UserStore");
const ClanStore = require("./storage/ClanStore");
const { TypeError } = require("./errors");

/** Provides data from War Thunder */
class ThunderAPI extends EventEmitter {
  /**
   * Creates a new instance of ThunderAPI
   * @param {ThunderAPIOptions} [options={}] The timeout in milliseconds to sweep the cache, defaults to 180000 milliseconds
   */
  constructor(options = { cacheSweepInterval: 180000 }) {
    super(options);
    if (typeof options.cacheSweepInterval !== "number") throw new TypeError("INVALID_CONSTRUCTOR_OPTION", "cacheSweepInterval", "number");

    /**
     * The Request Handler for ThunderApi
     * @type {RequestHandler}
     * @private
     */
    this.rest = new RESTManager();

    /**
     * A collection of cached profiles.
     * The collection will be sweeped after the time passed
     * @type {Collection<string,Profile>}
     */
    this.users = new UserStore(this);

    /**
     * A collection of cached squadrons.
     * The collection will be sweeped after the time passed
     * @type {Collection<string,Clan>}
     */
    this.clans = new ClanStore(this);

    /**
     * The time cache should be sweeped, in milliseconds
     * @type {number}
     * @readonly
     */
    this.cacheSweepInterval = options.cacheSweepInterval;

    /**
     * The cached array of news items
     * @type {?Array<NewsInfo>}
     */
    this.lastNews = null;

    /**
     * Timeouts set by {@link ThunderAPI#setTimeout} that are still active
     * @type {Set<Timeout>}
     * @private
     */
    this._timeouts = new Set();

    /**
     * Intervals set by {@link ThunderAPI#setInterval} that are still active
     * @type {Set<Timeout>}
     * @private
     */
    this._intervals = new Set();

    // Sweep cache every 180 seconds/time specified in constructor
    this.setInterval(this.sweepCache.bind(this), this.cacheSweepInterval);
  }

  /**
   * Get a player's profile
   * @param {string} player The profile of the player to fetch
   * @param {boolean} [cache=true] If it should get the player from the cache. Defaults to true
   * @return {Promise<User>}
   * @example
   * // The following example gets the profile of the player
   * // TheDutchy0412 and logs the squadron name
   * // and the registration date
   * ThunderAPI.getPlayer("TheDutchy0412")
   *   .then(profile => {
   *     console.log(profile.squadron);
   *     console.log(profile.registered);
   *   })
   *   .catch(err => console.error("Oh no, an error occurred!", err));
   */
  fetchUser(player, cache = true) {
    return this.users.fetch(player, cache);
  }

  /**
   * Searches on the War Thunder wiki
   * @param {string} query The query to search on
   * @return {Promise<Wiki[]>}
   */
  async searchWiki(query) {
    if (typeof query !== "string") return Promise.reject(new TypeError("INVALID_QUERY", "query", "string"));
    query = query.split(" ").join("+");
    // eslint-disable-next-line max-len
    const data = await this.rest.request("get", "/index.php", "wiki", { api: "wiki", query: { title: "Special:Search", search: query, fulltext: "Search" } })
      .catch(error => Promise.reject(error));

    return Promise.resolve(data);
  }

  /**
   * Verifies the given player's in-game nickname
   * @param {string} player The player to verify
   * @return {Promise<boolean>}
   */
  isUser(player) {
    if (typeof player !== "string") return Promise.reject(new TypeError("INVALID_QUERY", "player", "string"));
    if (this.users.has(player)) return Promise.resolve(true);
    return this.fetchUser(player)
      .then(() => true)
      .catch(() => false);
  }

  /**
   * Get's info about a squadron.
   * <note>You must provide the **full** name of the squadron,
   * e.g. "35th Gopnik nation battle group" instead of
   * "GOPNK".</note>
   * @param {string} name The **full** name of the squadron
   * @param {boolean} [cache=true] If it should get the squadron from the cache, if cached. Defaults to true
   * @return {Promise<Clan>}
   * @example
   * // The following example gets info about
   * // the squadron 35th Gopnik nation battle group,
   * // and logs the squadron description, and the
   * // date this squadron was created
   * ThunderAPI.getSquadron("35th Gopnik nation battle group")
   *   .then(data => {
   *     console.log(data.description);
   *     console.log(data.createdAt);
   *   })
   *   .catch(err => console.error("Oh no, an error occurred!", err));
   */
  fetchClan(name, cache = true) {
    return this.clans.fetch(name, cache);
  }

  /**
   * Returns an array of news objects
   * @param {boolean} [cache=true] If it should return the cached news object. Defaults to true
   * @return {Promise<NewsInfo[]>}
   */
  async getNews(cache) {
    if (this.lastNews && cache) return Promise.resolve(this.lastNews);
    const data = await this.rest.request("get", "/news3-en.html", "news")
      .catch(error => Promise.reject(error));
    this.lastNews = data;
    return Promise.resolve(data);
  }

  /**
   * Sweeps the cache
   * @return {boolean} If the cache sweep was successful
   */
  sweepCache() {
    try {
      this.users.clear();
      this.clans.clear();
      if (this.lastNews) this.lastNews = null;
      return true;
    } catch (err) {
      return false;
    }
  }
  /**
   * Destroys all assets used by ThunderAPI.
   */
  destroy() {
    for (const t of this._timeouts) clearTimeout(t);
    for (const i of this._intervals) clearInterval(i);
    this._timeouts.clear();
    this._intervals.clear();
  }

  /**
   * Sets a timeout that will be automatically cancelled if the instance of ThunderAPI is destroyed.
   * @param {Function} fn Function to execute
   * @param {number} delay Time to wait before executing (in milliseconds)
   * @param {...*} args Arguments for the function
   * @returns {Timeout}
   */
  setTimeout(fn, delay, ...args) {
    const timeout = setTimeout(() => {
      fn(...args);
      this._timeouts.delete(timeout);
    }, delay);
    this._timeouts.add(timeout);
    return timeout;
  }

  /**
   * Clears a timeout.
   * @param {Timeout} timeout Timeout to cancel
   */
  clearTimeout(timeout) {
    clearTimeout(timeout);
    this._timeouts.delete(timeout);
  }

  /**
   * Sets an interval that will be automatically cancelled if the client is destroyed.
   * @param {Function} fn Function to execute
   * @param {number} delay Time to wait between executions (in milliseconds)
   * @param {...*} args Arguments for the function
   * @returns {Timeout}
   */
  setInterval(fn, delay, ...args) {
    const interval = setInterval(fn, delay, ...args);
    this._intervals.add(interval);
    return interval;
  }

  /**
   * Clears an interval.
   * @param {Timeout} interval Interval to cancel
   */
  clearInterval(interval) {
    clearInterval(interval);
    this._intervals.delete(interval);
  }
}

module.exports = ThunderAPI;
module.exports.version = Package.version;
// Still exporting for backwards compatibility, probably remove it in the future!
module.exports.ThunderAPI = ThunderAPI;