registry/Registry.js

/**
 * @copyright 2016 Tridium, Inc. All Rights Reserved.
 * @author Logan Byam
 */

/**
 * API Status: **Development**
 * @module bajaux/registry/Registry
 */
define([ 'Promise',
        'underscore',
        'nmodule/js/rc/tinyevents/tinyevents',
        'bajaux/registry/RegistryEntry',
        'bajaux/registry/impl/JsRegistry' ], function (
         Promise,
         _,
         tinyevents,
         RegistryEntry,
         JsRegistry) {

  'use strict';

  /**
   * Query parameters to narrow the results resolved for a given registry key.
   *
   * @typedef {Object} module:bajaux/registry/Registry~QueryParams
   * @property {String} [rjs] If given, only resolve entries that have this
   * particular RequireJS ID.
   * @property {Array.<Array.<String>>} [deps] If given, only resolve entries that have
   * this exact set of RequireJS dependencies.
   * @property {Array.<String>} [hasAny] If given, only resolve entries that
   * have at least one of these `tags`.
   * @property {Array.<String>} [hasAll] If given, only resolve entries that
   * have all of these `tags`.
   */

  /**
   * Base class for Registry implementations (local JS registration,
   * agent-based, etc).
   *
   * @class
   * @alias module:bajaux/registry/Registry
   * @param {Object} [obj] a JSON object to use to initially build this registry
   * (typically will be used to reconstitute a registry using the previous
   * output of `toJSON`). If omitted, registry will be empty on creation.
   * @since Niagara 4.10
   */
  class Registry {
    constructor(obj) {
      this.$memoize();
      tinyevents(this);
      const reg = this.$localReg = new JsRegistry((obj || {}).localReg);
      reg.valueToKey = (value) => this.valueToKey(value);
    }

    /**
     * @returns {module:bajaux/registry/Registry}
     */
    getLocal() {
      return this.$localReg;
    }

    /**
     * Register a RequireJS module ID locally for the given key.
     *
     * @param {String} key
     * @param {module:bajaux/registry/Registry~QueryParams} [params]
     * query parameters/metadata for this RequireJS module
     * @returns {Promise} promise to be resolved after registration is complete
     */
    register(key, params) {
      return Promise.try(() => {
        this.$localReg.register(key, params);
        //local registrations can happen after querying the registry, so we must
        //clear memoization caches to ensure the registration takes effect.
        this.$clearMemoization();
        this.emit('changed');
      });
    }

    valueToKey(value) {
      return String(value);
    }

    /**
     * By default, just queries from the entries registered locally. Most likely,
     * subclasses will override this with something more useful.
     *
     * @param {*} value
     * @param {module:bajaux/registry/Registry~QueryParams} params
     * @returns {Promise.<Array.<module:bajaux/registry/RegistryEntry>>}
     * promise to be resolved with an array of all matching `RegistryEntry`s
     */
    queryAll(value, params) {
      return Promise.resolve(this.$localReg.queryAll(value, params));
    }

    /**
     * By default, just queries from the entries registered locally. Most likely,
     * subclasses will override this with something more useful.

     * @param {*} value
     * @param {module:bajaux/registry/Registry~QueryParams} params
     * @returns {Promise.<module:bajaux/registry/RegistryEntry>}
     * promise to be resolved with the first matching`RegistryEntry`
     */
    queryFirst(value, params) {
      return Promise.resolve(this.$localReg.queryFirst(value, params));
    }

    /**
     * Perform a query on the registry and resolve all RequireJS modules
     * represented. This will resolve an array of Widget constructors, menu
     * agent functions, etc.
     *
     * @param {*} value
     * @param {module:bajaux/registry/Registry~QueryParams} params
     * @returns {Promise.<Array.<*>>} promise to be resolved with an array of the
     * exported results of all RequireJS modules represented, or empty if none
     * found; rejected if the station could not be successfully queried for
     * registry info, or if any of the RequireJS modules failed to resolve
     */
    resolveAll(value, params) {
      return this.queryAll(value, params).then(resolveThemAll);
    }

    /**
     * Perform a query on the registry and attempt to resolve the first
     * matching entry's RequireJS module. Note that this differs from `resolveAll`
     * in that if the first entry fails to resolve (for instance, an invalid
     * RequireJS module ID), it will move on to the next entry and keep trying
     * to resolve all the way down until it can resolve *something*.
     *
     * @param {*} value
     * @param {module:bajaux/registry/Registry~QueryParams} params
     * @returns {Promise.<*>} promise to be resolved with the exported
     * results of the first matching entry that successfully resolves its
     * RequireJS module ID, or undefined if none found
     */
    resolveFirst(value, params) {
      return this.queryFirst(value, params)
        .then((entry) => entry &&
          entry.resolve().catch(() => this.queryAll(value, params).then(resolveFirst)));
    }

    /**
     * Return an object suitable for serialization using `JSON.stringify` or
     * similar. The returned object can be passed right back to a
     * `Registry` constructor to reconstitute later.
     *
     * @returns {Object}
     */
    toJSON() {
      return { localReg: this.$localReg.toJSON() };
    }

    /**
     * Set up memoizing on all registry query calls. Since registry contents
     * do not change at runtime, this should be safe to call.
     * @private
     */
    $memoize() {
      this.queryAll = _.memoize(this.queryAll, hashKeyAndParams);
      this.queryFirst = _.memoize(this.queryFirst, hashKeyAndParams);
      this.resolveAll = _.memoize(this.resolveAll, hashKeyAndParams);
      this.resolveFirst = _.memoize(this.resolveFirst, hashKeyAndParams);
    }

    /**
     * Clear memoization caches.
     * @private
     */
    $clearMemoization() {
      if (!this.queryAll.cache) {
        return; //memoization not done
      }
      this.queryAll.cache = {};
      this.queryFirst.cache = {};
      this.resolveAll.cache = {};
      this.resolveFirst.cache = {};
    }
  }

////////////////////////////////////////////////////////////////
// Support functions
////////////////////////////////////////////////////////////////

  function hashKeyAndParams(key, params) {
    return key + ',' + RegistryEntry.$hashParams(params);
  }

  function resolveIt(entry) { return entry.resolve(); }
  function resolveThemAll(entries) { return Promise.all(_.map(entries, resolveIt)); }

  /**
   * Resolve the first entry in the list. If it fails to resolve, walk down
   * the list until we find one that does. If all fail to resolve, just resolve
   * falsy.
   *
   * @inner
   * @param {Array.<module:bajaux/registry/RegistryEntry>} entries
   * @returns {Promise}
   */
  function resolveFirst(entries) {
    return Promise.resolve(entries.length && entries[0].resolve())
      .catch(() => resolveFirst(entries.slice(1)));
  }

  return Registry;
});