mixin/batchLoadMixin.js

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

/**
 * @module bajaux/mixin/batchLoadMixin
 */
define([ 'baja!',
        'bajaux/Widget',
        'Promise',
        'underscore' ], function (
         baja,
         Widget,
         Promise,
         _) {
  
  'use strict';
  
  var MIXIN_NAME = 'batchLoad',
      COMMIT_READY = 'commitReady';

  function batchLoad(ed, value, batch) {
    var readyToCommit,
        params = { batch: batch };

    if (ed.hasMixIn(MIXIN_NAME)) {
      // eslint-disable-next-line promise/avoid-new
      readyToCommit = new Promise(function (resolve, reject) {
        params.progressCallback = function (msg) {
          if (msg === COMMIT_READY) { resolve(); }
        };
      });
    }

    return [ readyToCommit, ed.load(value, params) ];
  }

  /**
   * Applies the `batchLoad` mixin to the target Widget.
   *
   * The `batchLoad` mixin does not alter the behavior of the target Widget,
   * but instead defines a behavioral contract. It defines the way it will
   * handle a `baja.comm.Batch` passed to the `load()` method (thus allowing
   * multiple `Widget`s to load and subscribe BajaScript values in a single
   * network call).
   *
   * It states:
   *
   * - If my `load()` method does receive a `batch` parameter, and does add
   *   a transaction to it (say, by passing it to 
   *   `baja.comm.Subscriber#subscribe`), then I must notify the caller after I
   *   am through adding transactions to that `Batch` and it is safe to commit.
   *   I do this by checking for a `progressCallback` parameter, and passing
   *   `COMMIT_READY` to it.
   * - If my `load()` method does not make use of the batch, it must still
   *   emit `COMMIT_READY`, but can do so at any time. (Due to this constraint,
   *   it does not make sense to add `batchLoadMixin` to a widget that does not
   *   actually use a batch.)
   *
   * Widgets that append transactions to a `batch` parameter in the `load()`
   * function, _without_ marking themselves with this mixin, should be expected
   * have those loads fail. Likewise, passing a batch to a Widget's `load()`
   * function without checking whether it has the `batchLoad` mixin can also
   * fail.
   * 
   * Why is this contract necessary? When passing a batch to `load()`, you
   * aren't guaranteed that `load()` will not perform some other asynchronous
   * work before appending transactions to the batch. If you don't wait for
   * the transactions to complete, you run the risk of committing the batch
   * prematurely. Then when the widget gets around to appending transactions
   * to the already-committed batch, it will fail.
   * 
   * To make this easier, `batchLoadMixin.loadWidgets` handles a lot of this
   * workflow for you.
   *
   * @class
   * @alias module:bajaux/mixin/batchLoadMixin
   * @param {module:bajaux/Widget} target
   *
   * @example
   * <caption>Example implementation of the batchLoad contract.</caption>
   * MyWidget.prototype.doLoad = function (component, params) {
   *   var batch = params && params.batch,
   *       progressCallback = params && params.progressCallback,
   *       promise = this.getSubscriber().subscribe({
   *         comps: component,
   *         batch: batch
   *       });
   *   
   *   //I'm done with the batch - let the caller know they can commit it
   *   if (progressCallback) {
   *     progressCallback(batchLoadMixin.COMMIT_READY);
   *   }
   *   
   *   return promise;
   * };
   */
  var batchLoadMixin = function (target) {
    if (!(target instanceof Widget)) {
      throw new Error("batchLoad mixin only applies to instances or sub-classes of Widget");
    }

    var mixins = target.$mixins;
    
    if (!_.contains(mixins, MIXIN_NAME)) {
      mixins.push(MIXIN_NAME);
    }
  };
  
  /**
   * Loads values into the given widgets, passing one `Batch` into the `load()`
   * method for each one.
   *
   * Widgets that make use of the `Batch` are expected to have `batchLoadMixin`.
   * See documentation for the mixin itself for contractual details.
   *
   * @param {Array.<module:bajaux/Widget>} widgets the widgets to load
   * @param {Array.<*>} values values to load into the widgets
   * @param {Object} [params]
   * @param {baja.comm.Batch} [params.batch] a batch to pass into each widget's
   * `load` method. If none is given, a new batch will be created and committed.
   * @param {Function} [params.progressCallback] This callback function itself
   * will receive `COMMIT_READY` when the input batch is ready to commit.
   * The callback will not be fired if no batch is input.
   * @returns {Promise} promise to be resolved when all widgets have completed
   * loading
   */
  batchLoadMixin.loadWidgets = function (widgets, values, params) {
    if (widgets.length !== values.length) {
      return Promise.reject(new Error('different numbers of widgets and values'));
    }
    
    var batchParam = params && params.batch,
        progressCallback = params && params.progressCallback,
        batch = batchParam || new baja.comm.Batch();

    var results = _.map(widgets, function (kid, i) {
          return batchLoad(kid, values[i], batch);
        }),
        loadPromises = _.map(results, function (arr) { return arr[1]; }),
        commitPromises = _.map(results, function (arr) { return arr[0]; });

    //widgets will tell us when they've registered network calls with
    //the batch and are ready for us to commit it.
    return Promise.all([
      Promise.all(commitPromises)
        .then(function () {
          if (!batchParam) {
            batch.commit();
          }
          if (progressCallback) {
            progressCallback(COMMIT_READY);
          }
        }),
      Promise.all(loadPromises)
    ]);
  };

  /**
   * Value to be passed to a `progressCallback` parameter to indicate that
   * a batch given to the `load()` function can be safely committed.
   * @constant
   * @type {string}
   */
  batchLoadMixin.COMMIT_READY = COMMIT_READY;

  return batchLoadMixin;
});