mixin/batchSaveMixin.js

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

/**
 * @module bajaux/mixin/batchSaveMixin
 */
define([ 'baja!',
        'bajaux/Widget',
        'Promise',
        'underscore' ], function (
         baja,
         Widget,
         Promise,
         _) {

  'use strict';
  
  var MIXIN_NAME = 'batchSave',
      COMMIT_READY = 'commitReady';

  function batchSave(ed, 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.save(params) ];
  }

  /**
   * Applies the `batchSave` mixin to the target Widget.
   * 
   * The `batchSave` 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 `save()` method (thus allowing
   * multiple `Widget`s to save BajaScript values in a single network call).
   * 
   * It states:
   *
   * - If my `save()` method does receive a `batch` parameter, and does add
   *   a transaction to it (say, by passing it to `Component#set`), 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 `save()` 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 `batchSaveMixin` to a widget that does not
   *   actually use a batch.)
   *   
   * Widgets that append transactions to a `batch` parameter in the `save()`
   * function, _without_ marking themselves with this mixin, should be expected
   * have those saves fail. Likewise, passing a batch to a Widget's `save()`
   * function without checking whether it has the `batchSave` mixin can also
   * fail.
   * 
   * Why is this contract necessary? When passing a batch to `save()`, you
   * aren't guaranteed that `save()` 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, `batchSaveMixin.saveWidgets` handles a lot of this
   * workflow for you.
   * 
   * @class
   * @alias module:bajaux/mixin/batchSaveMixin
   * @param {module:bajaux/Widget} target
   * 
   * @example
   * <caption>Example implementation of the batchSave contract.</caption>
   * MyWidget.prototype.doSave = function (component, params) {
   *   var batch = params && params.batch,
   *       progressCallback = params && params.progressCallback,
   *       promise = component.set({ slot: 'saved', value: true, batch: batch });
   *   
   *   //I'm done with the batch - let the caller know they can commit it
   *   if (progressCallback) {
   *     progressCallback(batchSaveMixin.COMMIT_READY);
   *   }
   *   
   *   return promise;
   * };
   */
  var batchSaveMixin = function batchSaveMixin(target) {
    if (!(target instanceof Widget)) {
      throw new Error("batchSave mixin only applies to instances or sub-classes of Widget");
    }

    var mixins = target.$mixins;

    if (!_.contains(mixins, MIXIN_NAME)) {
      mixins.push(MIXIN_NAME);
    }
  };

  /**
   * Saves the given widgets, passing one `Batch` into the `save()` method for
   * each one.
   * 
   * Widgets that make use of the `Batch` are expected to have `batchSaveMixin`.
   * See documentation for the mixin itself for contractual details.
   * 
   * @param {Array.<module:bajaux/Widget>} widgets the widgets to save
   * @param {Object} [params]
   * @param {baja.comm.Batch} [params.batch] a batch to pass into each widget's
   * `save` 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
   * saving
   */
  batchSaveMixin.saveWidgets = function saveWidgets(widgets, params) {
    var batchParam = params && params.batch,
        progressCallback = params && params.progressCallback,
        batch = batchParam || new baja.comm.Batch();

    var results = _.map(widgets, function (kid) {
        return batchSave(kid, batch);
      }),
      savePromises = _.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.
    Promise.all(commitPromises)
      .then(function () {
        if (!batchParam) {
          batch.commit();
        }
        if (progressCallback) {
          progressCallback(COMMIT_READY);
        }
      })
      .catch((err) => baja.error(err));

    return Promise.all(savePromises);
  };

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