platanus/angular-restmod

View on GitHub
src/plugins/debounced.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * @mixin DebouncedModel
 *
 * @description Adds debouncing to `$save` operations so rapid consecutive calls result in only one server request.
 *
 * Usage:
 *
 * First add mixin to a model's mixin chain, the following config variables are available:
 * * DM_TIMEOUT: sets the debounce timeout, if 0 then debouncing is deactivated. Defaults to 500
 * * DM_ADJOURN: if true, then save operation is rescheduled on every $save call. Default to false
 *
 * ```javascript
 * var Bike = restmod.model('api/bikes', 'DebouncedModel', {
 *       // This is optional.
 *       DM_TIMEOUT: 100 // change timeout!
 *     }),
 *     bike = Bike.build({ id: 1, brand: 'Yeti' });
 * ```
 *
 * Then use `$save` as always
 *
 * ```javascript
 * // The following code will just generate 1 request
 * bike.$save();
 * bike.$save();
 * bike.$save();
 * ```
 *
 * Or with options
 *
 * ```javascript
 * bike.$save({ timeout: 0, adjourn: false });
 * // same as
 * bike.$saveNow();
 * ```
 *
 */

'use strict';

var isObject = angular.isObject;

angular.module('restmod').factory('DebouncedModel', ['restmod', '$timeout', '$q', function(restmod, $timeout, $q) {

  // builds a new async save function bound to a given context and promise.
  function buildAsyncSaveFun(_this, _oldSave, _promise, _oldPromise) {
    return function() {

      // swap promises so save behaves like it has been called during the original call.
      var currentPromise = _this.$promise;
      _this.$promise = _oldPromise;

      // when save resolves, the timeout promise is resolved and the last resource promise returned
      // so it behaves
      _oldSave.call(_this).$promise.then(
        function(_data) {
          _promise.resolve(_data);
          _this.$promise = currentPromise;
        }, function(_reason) {
          _promise.reject(_reason);
          _this.$promise = currentPromise;
        }
      );

      _this.$dmStatus = null;
    };
  }

  return restmod.mixin(function() {
    this.setProperty('dmTimeout', 500)
        .setProperty('dmAdjourn', true)

        /**
         * @method $save
         * @memberOf DebouncedModel
         *
         * @description Debounced `$save` implementation
         *
         * IDEA: think of a way of separating the scheduling-rescheduling logic from
         * the async save implementation, this way it can be used for other actions.
         * Something like:
         *
         *    this.$debounce('$save', fun, timeout, adjourn);
         *
         * This would call fun with a promise in the model context.
         *
         * @param {object} _opt Same as `setDebounceOptions` options.
         * @return {Model} self
         */
        .define('$save', function(_opt) {

          var timeout = this.$type.getProperty('dmTimeout'),
              adjourn = this.$type.getProperty('dmAdjourn'),
              status = this.$dmStatus;

          // apply configuration overrides
          if(_opt !== undefined) {
            if(isObject(_opt)) {
              if(_opt.timeout !== undefined) timeout = _opt.timeout;
              if(_opt.adjourn !== undefined) adjourn = _opt.adjourn;
            }
          }

          if(!status) {

            // if timeout is set to 0, then just call save inmediatelly.
            if(!timeout) return this.$super();

            var deferred = $q.defer(),
                asyncSave = buildAsyncSaveFun(this, this.$super, deferred, this.$promise);

            this.$dmStatus = {
              save: asyncSave,
              promise: deferred.promise,
              timeout: $timeout(asyncSave, timeout)
            };

            this.$promise = deferred.promise;

          } else {

            // reschedule only if adjourn hasnt been deactivated.
            if(adjourn) {
              $timeout.cancel(status.timeout);

              // depending on timeout schedule or save inmediatelly.
              if(timeout) {
                status.timeout = $timeout(status.save, timeout);
              } else {
                status.save();
              }
            }

            // keep the last promise.
            this.$promise = status.promise;
          }

          return this;
        })

        /**
         * @method $saveNow
         * @memberOf DebouncedModel
         *
         * @description Convenience method that will cancel any pending save and save inmediatelly.
         *
         * @return {Model} self
         */
        .define('$saveNow', function() {
          return this.$save({ timeout: 0, adjourn: true });
        });
  });
}]);