Source: environment/EnvironmentList.js

// import dependencies
import {fetch} from '../utils/contextuals'
import Environment from './Environment';
import defaults from './defaults';
import {Environments} from '../constants';

/**
 * Loads the available environments from the CDN, providing an array of {@link Environment} instances to interface with.
 * The EnvironmentList uses a singleton instance to interface with the environments, unlike a {@link VersionList}.
 */
class EnvironmentList {
  constructor () {
    this._environments = defaults.map(env => {
      return new Environment(env)
    })

    // load asynchronously
    this._loaded = false
  }

  async _loadRemoteEnvironments ({cacheBust}) {
    try {
      const remoteEnvironments = await fetch(`https://cdn.medal.tv/public/environments.json${cacheBust ? `?t=${Date.now()}` : ''}`);
      const remoteEnvironmentsJson = await remoteEnvironments.json();
      this._environments = remoteEnvironmentsJson.map(env => {
        return new Environment(env)
      })
    } catch (err) {
      if (err.toString().includes('ENOTFOUND')) {
        throw new Error(`Failed to retrieve environments list`)
      } else {
        throw err;
      }
    }

    // map back to original format, but now sorted
    return this._environments;
  }

  /**
   * Load the remote environments list from the CDN if not already
   * loaded and not forcing a reload.
   * @returns {Promise<EnvironmentList>}
   */
  async load (force = false, cacheBust = false) {
    if (this._loaded && !force) {
      return this;
    }
    await this._loadRemoteEnvironments({cacheBust});
    this._loaded = true;
    return this;
  }

  /**
   * Get an {@link Environment} by value, e.g. 'production' or 'staging'.
   * @param environmentValue
   * @returns {Environment}
   */
  getEnvironment (environmentValue) {
    return this._environments.find(env => env.value === environmentValue)
  }

  /**
   * Get the {@link Environment} by value, or fallback to the appropriate one
   * by inferring the intended fallback based on the environment value.
   *
   * Any environments with 'sandbox' in the string will fallback to sandbox.
   * Any environments with 'beta' in the string will fallback to early access.
   * All other environments that no longer exist will fallback to production.
   *
   * If the 'requireModule' option is set to 'true', the environment will fallback
   * if the target environment exists, but it doesn't include any builds for the
   * specified module.
   *
   * @param environmentValue
   * @param requireModule
   * @returns {Environment}
   */
  getEnvironmentOrFallback (environmentValue, {requireModule} = {}) {
    let environment = this.getEnvironment(environmentValue);
    if (!environment || (!!requireModule && !environment.includesModule(requireModule))) {
      return this.getFallback(environmentValue);
    }
    return environment;
  }

  /**
   * Get the fallback environment, this assumes that it is always a beta or dynamic
   * environment that is falling back. Static environments won't have a fallback.
   * @param environmentValue
   * @returns {Environment}
   */
  getFallback (environmentValue) {
    const inferIsBeta = environmentValue.includes('-beta');
    if (inferIsBeta || environmentValue === Environments.EARLY_ACCESS_CANDIDATE) {
      return this.getEnvironment(Environments.EARLY_ACCESS);
    } else if (environmentValue === Environments.PRODUCTION_CANDIDATE) {
      return this.getEnvironment(Environments.PRODUCTION);
    }
    return this.getEnvironment(Environments.SANDBOX);
  }

  /**
   * Return the {@link Environment} for the specified origin, if it exists.
   * @param origin
   * @returns {Environment}
   */
  getEnvironmentByOrigin (origin) {
    return this._environments.find(env => env.origin === origin);
  }

  /**
   * Get the latest array of {@link Environment} instances (including any modifications).
   * @returns {Environment[]}
   */
  getEnvironments () {
    if (!this._loaded) {
      console.warn('EnvironmentList not loaded yet!');
    }
    return this._environments;
  }

  /**
   * Return only public {@link Environment} instances.
   * @returns {Environment[]}
   */
  getPublicEnvironments () {
    return this._environments.filter(env => env.public === true);
  }

  /**
   * Return only static {@link Environment} instances.
   * @returns {Environment[]}
   */
  getStaticEnvironments () {
    return this._environments.filter(env => env.static === true);
  }

  /**
   * Return true if the environment list contains the specified environment (by string or by {@link Environment}
   * @param {string|Environment} environment
   * @returns {boolean}
   */
  includes (environment) {
    if (environment instanceof Environment) {
      environment = environment.getValue();
    }
    return !!this._environments.find(env => env.value === environment);
  }

  /**
   * Add a new {@link Environment} to the {@link EnvironmentList}.
   * @param {Environment} environment
   * @returns {EnvironmentList}
   */
  add (environment) {
    if (!this._loaded) {
      throw new Error('List not loaded yet!')
    }

    if (this._environments.find(env => env.value === environment.value)) {
      throw new Error('Environments list already contains environment: ' + environment.value)
    }

    // create a new array from the remote versions
    this._environments.push(environment);
    return this
  }

  /**
   * Remove an {@link Environment} from the {@link EnvironmentList}.
   * @param {Environment} environment
   * @returns {EnvironmentList}
   */
  remove (environment) {
    if (environment.isStatic() && !environment.isExperimental()) {
      throw new Error(`Cannot remove static non-experimental environment: ${environment.getValue()}`)
    }
    this._environments = this._environments.filter(env => env.getValue() !== environment.getValue());
    return this;
  }

  /**
   * Convert the {@link EnvironmentList} and it's {@link Environment} instances to a serializable object.
   */
  toJson () {
    return this._environments.map(environment => environment.toJson())
  }
}

// we can instantiate this once to keep a singleton instance of environments
const environmentList = new EnvironmentList();

// compatibility with browser context
module.exports = environmentList

export default environmentList