baja/ord/OrdCoalescer.js

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

/* eslint-env browser */

/**
 * @private
 * @module baja/ord/OrdCoalescer
 */
define([
  'bajaScript/comm',
  'bajaScript/baja/ord/OrdTarget',
  'bajaScript/baja/ord/ordUtil',
  'bajaPromises' ], function (
  baja,
  OrdTarget,
  ordUtil,
  Promise) {

  'use strict';

  var resolveOptimized = ordUtil.resolveOptimized;

  function waitInterval(delay) {
    var df = Promise.deferred();
    // noinspection DynamicallyGeneratedCodeJS
    setTimeout(df.resolve, delay);
    return df.promise();
  }

  /**
   * This class handles requests to resolve ORDs. Unlike BajaScript itself,
   * which will happily resolve the exact same ORD any number of times, this
   * will introduce a small delay. Any duplicate ORD resolutions requested
   * during this delay will simply be bound together into a single network call
   * to resolve that one ORD.
   *
   * @private
   * @class
   * @alias module:baja/ord/OrdCoalescer
   * @param {object} [params]
   * @param {number} [params.delay=0] how many ms to hold outgoing ORD
   * resolutions before sending them across the network
   * @param {boolean} [params.provideSubstitutes] if true, the `OrdTarget`s
   * resolved by the coalescer will have substitute `optimizedOrd` facets for
   * future re-resolution.
   * @param {boolean} [params.viewQueryCoalesce] if true, the `OrdTarget`s
   * resolved by the coalescer will remove the ViewQuery when coalescing.
   */
  var OrdCoalescer = function OrdCoalescer(params) {
    this.$delay = (params && params.delay) || 0;
    this.$pendingNoSubscribe = {};
    this.$pendingSubscribe = {};
    this.$provideSubstitutes = params && params.provideSubstitutes;
    this.$viewQueryCoalesce = params && params.viewQueryCoalesce;
  };

  /**
   * @param {string|baja.Ord} ord
   * @param {object} [params]
   * @param {baja.Subscriber} [params.subscriber] provide if you wish for the
   * ORD to be subscribed
   * @returns {Promise.<module:baja/ord/OrdTarget>}
   */
  OrdCoalescer.prototype.resolve = function (ord, params) {
    var that = this;
    var subscriber = params && params.subscriber;
    var df = Promise.deferred();
    df.subscriber = subscriber;
    var ordInfo;
    if (this.$viewQueryCoalesce) {
      ordInfo = ordUtil.getOrdViewQuerySplit(ord);
    }

    var resolution = that.$getQueuedResolution((ordInfo && ordInfo.ord) || ord, !!subscriber);
    resolution.deferreds.push(df);
    return this.$requestFlush()
      .then(function () {
        return df.promise();
      })
      .then(function (ordTarget) {
        ordTarget = ordTarget.clone();
        if (ordInfo && ordInfo.viewQuery) {
          ordUtil.appendViewQueryToOrdTarget(ordTarget, ordInfo.viewQuery);
        }
        return ordTarget;
      });
  };

  /**
   * @private
   * @param {string|baja.Ord} ord
   * @param {boolean} subscribe
   * @returns {module:baja/ord/OrdCoalescer~QueuedResolution}
   */
  OrdCoalescer.prototype.$getQueuedResolution = function (ord, subscribe) {
    var pendingSubscribe = this.$pendingSubscribe;
    var pendingNoSubscribe = this.$pendingNoSubscribe;
    var subscribeResolution = pendingSubscribe[ord];
    var noSubscribeResolution = pendingNoSubscribe[ord];
    var resolution;

    if (subscribeResolution) {
      resolution = subscribeResolution;
    } else {
      if (subscribe) {
        if (noSubscribeResolution) {
          resolution = pendingSubscribe[ord] = noSubscribeResolution;
          delete pendingNoSubscribe[ord];
        } else {
          resolution = getResolution(pendingSubscribe, ord);
        }
      } else {
        resolution = getResolution(pendingNoSubscribe, ord);
      }
    }

    return resolution;
  };

  /**
   * Performs all queued ORD resolutions and resolves all promises waiting on
   * them. If called before the delay has expired, will wait for the delay to
   * elapse first.
   * @private
   * @returns {Promise}
   */
  OrdCoalescer.prototype.$requestFlush = function () {
    var that = this;
    var flushPromise = that.$flushPromise;
    if (flushPromise) { return flushPromise; }

    return (that.$flushPromise = waitInterval(that.$delay)
      .then(function () {
        return that.$flush();
      }));
  };

  /**
   * @private
   * @returns {Promise}
   */
  OrdCoalescer.prototype.$flush = function () {
    var that = this;
    var promise = Promise.all([
      that.$batchResolvePending(that.$pendingSubscribe, true),
      that.$batchResolvePending(that.$pendingNoSubscribe)
    ]);
    that.$pendingSubscribe = {};
    that.$pendingNoSubscribe = {};
    delete that.$flushPromise;
    return promise;
  };

  /**
   * @private
   * @param {Object<string, module:baja/ord/OrdCoalescer~QueuedResolution>} pending
   * @param {boolean} subscribe
   * @returns {Promise}
   */
  OrdCoalescer.prototype.$batchResolvePending = function (pending, subscribe) {
    var batchSubscriber = subscribe ? new baja.Subscriber() : undefined;
    var ords = Object.keys(pending);
    if (!ords.length) {
      return Promise.resolve();
    }

    return this.$batchResolve({ ords: ords, subscriber: batchSubscriber })
      .then(function (results) {
        return Promise.all(results.map(function (result, i) {
          var ord = ords[i];
          var isResolved = !(result instanceof Error);
          var deferreds = pending[ord].deferreds;
          return Promise.all(deferreds.map(function (df) {
            var resolve = df.resolve;
            var reject = df.reject;
            var subscriber = df.subscriber;

            if (!isResolved) {
              return reject(result);
            }

            var ordTarget = result;
            var component = ordTarget.getComponent();

            if (subscriber && isSubscribable(component)) {
              return subscriber.subscribe(component)
                .then(function () { resolve(ordTarget); });
            } else {
              resolve(ordTarget);
            }
          }));
        }));
      })
      .then(function () {
        return batchSubscriber && batchSubscriber.unsubscribeAll();
      });
  };

  /**
   * @private
   * @param {object} params
   * @param {Array.<string|baja.Ord>} params.ords
   * @param {baja.Subscriber} [params.subscriber]
   * @returns {Promise.<Array.<module:baja/ord/OrdTarget|Error>>} array of
   * results of each ord resolution - either an OrdTarget or Error instance
   */
  OrdCoalescer.prototype.$batchResolve = function (params) {
    var resolvePromise;
    if (this.$provideSubstitutes) {
      resolvePromise = resolveOptimized(params, {});
    } else {
      var br = new baja.BatchResolve(params.ords);
      resolvePromise = br.resolve({ subscriber: params.subscriber })
        .catch(function (ignore) {})
        .then(function () { return br; });
    }

    return resolvePromise
      .then(function (br) {
        return range(br.size()).map(function (i) {
          return br.isResolved(i) ? br.getTarget(i) : br.getFail(i);
        });
      });
  };

  /**
   * Resolution of a single ORD, that may satisfy any number of `resolve()`
   * calls for that particular ORD.
   *
   * @typedef module:baja/ord/OrdCoalescer~QueuedResolution
   * @property {Array.<module:baja/ord/OrdCoalescer~Deferred>} deferreds deferred
   * promises waiting on this one ORD to be resolved
   */

  /**
   * @typedef module:baja/ord/OrdCoalescer~Deferred
   * @property {function} resolve function to resolve the caller's promise
   * @property {function} reject function to reject the caller's promise
   * @property {baja.Subscriber} subscriber provided if the caller wishes to
   * subscribe the ORD
   */

  function getResolution(pending, ord) {
    return pending[ord] || (pending[ord] = { deferreds: [] });
  }

  function isSubscribable(val) {
    return baja.hasType(val, 'baja:Component') && val.isMounted();
  }

  function range(length) {
    var arr = new Array(length);
    for (var i = 0; i < length; ++i) { arr[i] = i; }
    return arr;
  }

  return OrdCoalescer;
});