• Jump To … +
    modules/Behavior.js modules/Cell.js modules/Collection.js modules/Events.js modules/FormModel.js modules/FormView.js modules/ListView.js modules/Model.js modules/NestedCell.js modules/NestedModel.js modules/Router.js modules/ServiceCell.js modules/View.js modules/behaviors/DataBehavior.js modules/configure.js modules/handlebarsUtils.js modules/history.js modules/mixins/cacheMixin.js modules/mixins/cellMixin.js modules/mixins/loadingMixin.js modules/mixins/modelMixin.js modules/mixins/pollingMixin.js modules/registry.js modules/stickitUtils.js modules/templateRenderer.js modules/torso.js modules/validation.js
  • cacheMixin.js

  • ¶
    (function(root, factory) {
      if (typeof define === 'function' && define.amd) {
        define(['underscore', 'backbone'], factory);
      } else if (typeof exports === 'object') {
        module.exports = factory(require('underscore'), require('backbone'));
      } else {
        root.Torso = root.Torso || {};
        root.Torso.Mixins = root.Torso.Mixins || {};
        root.Torso.Mixins.cache = factory(root._, root.Backbone);
      }
    }(this, function(_, Backbone) {
    
      var $ = Backbone.$;
    
      /**
       * Custom additions to the Backbone Collection object.
       * - safe disposal methods for memory + event management
       * - special functional overrides to support ID registration for different views
       *
       * @mixin cacheMixin
       *
       * @author ariel.wexler@vecna.com, kent.willis@vecna.com
       *
       * @see <a href="../annotated/modules/mixins/cacheMixin.html">cacheMixin Annotated Source</a>
       */
      var mixin = function(base) {
    
        var cacheMixin, createRequesterCollectionClass;
    
        /**
         * Returns a new class of collection that inherits from the parent but not the cacheMixin
         * and adds a requesterMixin that connects this cache to it's parent
         *
         * @class PrivateCollection
         * @extends Collection
         * @param {external:Backbone-Collection} parent the parent of the private collection
         * @param {string} guid the unique code of the owner of this private collection
         */
        createRequesterCollectionClass = function(parent, guid) {
          return parent.constructor.extend((function(parentClass, parentInstance, ownerKey) {
    
            /**
             * A mixin that overrides base collection methods meant for cache's and tailors them
             * to a requester.
             * @alias PrivateCollection.prototype
             */
            var requesterMixin = {
    
              /**
               * @returns {Array} array of ids that this collection is tracking
               */
              getTrackedIds: function() {
                return this.trackedIds;
              },
    
              /**
               * Will force the cache to fetch just the registered ids of this collection
               * @param [options] - argument options
               * @param {Array} [options.idsToFetch=collectionTrackedIds] - A list of request Ids, will default to current tracked ids
               * @param {Object} [options.setOptions] - if a set is made, then the setOptions will be passed into the set method
               * @returns {Promise} promise that will resolve when the fetch is complete
               */
              fetch: function(options) {
                options = options || {};
                options.idsToFetch = options.idsToFetch || this.trackedIds;
                options.setOptions = options.setOptions || {remove: false};
                return this.__loadWrapper(function() {
                  if (options.idsToFetch && options.idsToFetch.length) {
                    return parentInstance.fetchByIds(options);
                  } else {
                    return $.Deferred().resolve().promise();
                  }
                });
              },
    
              /**
               * Will force the cache to fetch a subset of this collection's tracked ids
               * @param {Array} ids array of model ids
               * @param {Object} [options] if given, will pass the options argument to this.fetch. Note, will not affect options.idsToFetch
               * @returns {Promise} promise that will resolve when the fetch is complete
               */
              fetchByIds: function(ids, options) {
                options = options || {};
                options.idsToFetch = _.intersection(ids, this.getTrackedIds());
                return this.fetch(options);
              },
    
              /**
               * Pass a list of ids to begin tracking. This will reset any previous list of ids being tracked.
               * Overrides the Id registration system to route via the parent collection
               * @param ids The list of ids that this collection wants to track
               */
              trackIds: function(ids) {
                this.remove(_.difference(this.trackedIds, ids));
                parentInstance.registerIds(ids, ownerKey);
                this.trackedIds = ids;
              },
    
              /**
               * Adds a new model to the requester collection and tracks the model.id
               * @param {external:Backbone-Model} model the model to be added
               */
              addModelAndTrack: function(model) {
                this.add(model);
                parentInstance.add(model);
                this.trackNewId(model.id);
              },
    
              /**
               * Tracks a new id
               * @param {(string|Number)} id the id attribute of the model
               */
              trackNewId: function(id) {
                this.trackIds(this.getTrackedIds().concat(id));
              },
    
              /**
               * Will begin tracking the new ids and then ask the cache to fetch them
               * This will reset any previous list of ids being tracked.
               * @returns the promise of the fetch by ids
               */
              trackAndFetch: function(newIds) {
                this.trackIds(newIds);
                return this.fetch();
              },
    
              /**
               * Will force the cache to fetch any of this collection's tracked models that are not in the cache
               * while not fetching models that are already in the cache. Useful when you want the effeciency of
               * pulling models from the cache and don't need all the models to be up-to-date.
               *
               * If the ids being fetched are already being fetched by the cache, then they will not be re-fetched.
               *
               * The resulting promise is resolved when ALL items in the process of being fetched have completed.
               * The promise will resolve to a unified data property that is a combination of the completion of all of the fetches.
               *
               * @param {Object} [options] if given, will pass the options argument to this.fetch. Note, will not affect options.idsToFetch
               * @returns {Promise} promise that will resolve when the fetch is complete with all of the data that was fetched from the server.
               *                   Will only resolve once all ids have attempted to be fetched from the server.
               */
              pull: function(options) {
                options = options || {};
  • ¶

    find ids that we don’t have in cache and aren’t already in the process of being fetched.

                var idsNotInCache = _.difference(this.getTrackedIds(), _.pluck(parentInstance.models, 'id'));
                var idsWithPromises = _.pick(parentInstance.idPromises, idsNotInCache);
  • ¶

    Determine which ids are already being fetched and the associated promises for those ids.

                options.idsToFetch = _.difference(idsNotInCache, _.uniq(_.flatten(_.keys(idsWithPromises))));
                var thisFetchPromise = this.fetch(options);
  • ¶

    Return a promise that resolves when all ids are fetched (including pending ids).

                var allPromisesToWaitFor = _.flatten(_.values(idsWithPromises));
                allPromisesToWaitFor.push(thisFetchPromise);
                var allUniquePromisesToWaitFor = _.uniq(allPromisesToWaitFor);
                return $.when.apply($, allUniquePromisesToWaitFor)
  • ¶

    Make it look like the multiple promises was performed by a single request.

                  .then(function() {
  • ¶

    collects the parts of each ajax call into arrays: result = { [data1, data2, …], [textStatus1, textStatus2, …], [jqXHR1, jqXHR2, …] };

                    var result = _.zip(arguments);
  • ¶

    Flatten the data so it looks like the result of a single request.

                    var resultData = result[0];
                    var flattenedResultData = _.flatten(resultData);
                    return flattenedResultData;
                  });
              },
    
              /**
               * Will register the new ids and then pull in any models not stored in the cache. See this.pull() for
               * the difference between pull and fetch.
               * @returns the promise of the fetch by ids
               */
              trackAndPull: function(newIds) {
                this.trackIds(newIds);
                return this.pull();
              },
    
              /**
               * Handles the disposing of this collection as it relates to a requester collection.
               */
              requesterDispose: function() {
                parentInstance.removeRequester(ownerKey);
              },
    
              /**
               * In addition to removing the model from the collection also remove it from the list of tracked ids.
               * @param {*} modelIdentifier same duck-typing as Backbone.Collection.get():
               *                              by id, cid, model object with id or cid properties,
               *                              or an attributes object that is transformed through modelId
               */
              remove: function(modelIdentifier) {
                var model = this.get(modelIdentifier);
                parentClass.remove.apply(this, arguments);
                if (model) {
                  var trackedIdsWithoutModel = this.getTrackedIds();
                  trackedIdsWithoutModel = _.without(trackedIdsWithoutModel, model.id);
                  this.trackIds(trackedIdsWithoutModel);
                }
              }
            };
    
            return requesterMixin;
          })(parent.constructor.__super__, parent, guid));
        };
    
        /**
         * Adds functions to manage state of requesters
         *
         * @param {Collection} collection the collection to add this mixin
         */
        cacheMixin = function(collection) {
  • ¶

    ***** PRIVATE METHODS ****//

          /**
           * @alias cacheMixin.setRequestedIds
           * @private
           * @param {string} guid the global unique identifier for the requester
           * @param {Array} array the array of ids the requester wants
           */
          var setRequestedIds = function(guid, array) {
            collection.requestMap[guid] = {
              array: array,
              dict: _.object(_.map(array, function(id) { return [id, id]; }))
            };
          };
  • ¶

    *** PUBLIC METHODS ****//

          /**
           * @alias cacheMixin.getRequesterIds
           * @param {string} the global unique id of the requester
           * @returns {Array} an array of the ids the requester with the guid has requested
           */
          collection.getRequesterIds = function(guid) {
            return this.requestMap[guid] && this.requestMap[guid].array;
          };
    
          /**
           * This method is used for quick look up of a certain id within the list of requested ids
           * @alias cacheMixin.getRequesterIdsAsDictionary
           * @param {string} guid the global unique id of the requester
           * @returns {Object} an dictionary of id -> id of the requester ids for a given requester.
           */
          collection.getRequesterIdsAsDictionary = function(guid) {
            return this.requestMap[guid] && this.requestMap[guid].dict;
          };
    
          /**
           * Removes a requester from this cache. No longer receives updates
           * @alias cacheMixin.removeRequester
           * @param {string} guid the global unique id of the requester
           */
          collection.removeRequester = function(guid) {
            delete this.requestMap[guid];
            delete this.knownPrivateCollections[guid];
          };
    
          /**
           * NOTE: this methods returns only the guids for requester collections that are currently tracking ids
           * TODO: should this return just the knownPrivateCollections
           * @alias cacheMixin.getRequesters
           * @returns {Array} an array of the all requesters in the form of their GUID's
           */
          collection.getRequesters = function()  {
            return _.keys(this.requestMap);
          };
    
          /**
           * Return the list of Ids requested by this collection
           * @alias cacheMixin.getAllRequestedIds
           * @returns {Array} the corresponding requested Ids
           */
          collection.getAllRequestedIds = function() {
            return this.collectionTrackedIds;
          };
    
          /**
           * Used to return a collection of desired models given the requester object.
           * Binds a custom "resized" event to the private collections.
           * Overrides the fetch method to call the parent collection's fetchByIds method.
           * Overrides the registerIds method to redirect to its parent collection.
           * @alias cacheMixin.createPrivateCollection
           * @param {string} guid Identifier for the requesting view
           * @returns {PrivateCollection} an new empty collection of the same type as "this"
           */
          collection.createPrivateCollection = function(guid, args) {
            args = args || {};
            args.isRequester = true;
            args.parentInstance = collection;
            var RequesterClass = createRequesterCollectionClass(collection, guid);
            this.knownPrivateCollections[guid] = new RequesterClass(null, args);
            return this.knownPrivateCollections[guid];
          };
    
          /**
           * Registers a list of Ids that a particular object cares about and pushes
           * any cached models its way.
           *
           * This method intelligently updates the "_requestedIds" field to contain all unique
           * requests for Ids to be fetched.  Furthermore, the "polledFetch" method
           * is overriden such that it no longer routes through Backbone's fetch all,
           * but rather a custom "fetchByIds" method.
           * @alias cacheMixin.registerIds
           * @param {Array} newIds  - New ids to register under the requester
           * @param {string} guid   - The GUID of the object that wants the ids
           */
          collection.registerIds = function(newIds, guid) {
            var i, newIdx, model, requesterIdx, storedIds,
                requesters, requesterLength, privateCollection,
                models = [],
                distinctIds = {},
                result = [];
  • ¶

    Save the new requests in the map

            setRequestedIds(guid, newIds);
            requesters = collection.getRequesters();
            requesterLength = requesters.length;
  • ¶

    Collect all cached models

            for (newIdx = 0; newIdx < newIds.length; newIdx++) {
              model = collection.get(newIds[newIdx]);
              if (model) {
                models.push(model);
              }
            }
  • ¶

    Push cached models to the respective requester

            privateCollection = collection.knownPrivateCollections[guid];
            if (privateCollection) {
              privateCollection.set(models, {remove: false});
            }
  • ¶

    Create a new request list

            for (requesterIdx = 0; requesterIdx < requesterLength; requesterIdx++) {
              storedIds = this.getRequesterIds(requesters[requesterIdx]);
              if (!_.isUndefined(storedIds)) {
                for (i = 0; i < storedIds.length; i++) {
                  distinctIds[storedIds[i]] = true;
                }
              }
            }
  • ¶

    Convert the hash table of unique Ids to a list

            for (i in distinctIds) {
              result.push(i);
            }
            this.collectionTrackedIds = result;
  • ¶

    Overrides the polling mixin’s fetch method

            this.polledFetch = function() {
              collection.fetchByIds({
                setOptions: {remove: true}
              });
            };
          };
    
          /**
           * Overrides the base fetch call if this.fetchUsingTrackedIds is true
           * Calling fetch from the cache will fetch the tracked ids if fetchUsingTrackedIds is set to true, otherwise
           * it will pass through to the default fetch.
           * @alias cacheMixin.fetch
           * @param {Object} options
           */
          collection.fetch = function(options) {
            options = options || {};
            if (this.fetchUsingTrackedIds) {
              return this.fetchByIds({
                setOptions: _.extend({remove: true}, options)
              });
            } else {
              return base.prototype.fetch.call(this, options);
            }
          };
    
          /**
           * A custom fetch operation to only fetch the requested Ids.
           * @alias cacheMixin.fetchByIds
           * @param [fetchByIdsOptions] - argument fetchByIdsOptions
           * @param {Array} [fetchByIdsOptions.idsToFetch=collection.collectionTrackedIds] - A list of request Ids, will default to current tracked ids
           * @param {Object} [fetchByIdsOptions.setOptions] - if a set is made, then the setOptions will be passed into the set method
           * @returns {Promise} the promise of the fetch
           */
          collection.fetchByIds = function(fetchByIdsOptions) {
            fetchByIdsOptions = fetchByIdsOptions || {};
  • ¶

    Fires a method from the loadingMixin that wraps the fetch with events that happen before and after

            var requestedIds = fetchByIdsOptions.idsToFetch || collection.collectionTrackedIds;
            var fetchComplete = false;
            var fetchPromise = this.__loadWrapper(function(loadWrapperOptions) {
              var contentType = loadWrapperOptions.fetchContentType || collection.fetchContentType;
              var ajaxOpts = {
                  type: collection.fetchHttpAction,
                  url: _.result(collection, 'url') + collection.getByIdsUrl,
                  data: {ids: requestedIds.join(',')}
                };
              if (contentType || (ajaxOpts.type && ajaxOpts.type.toUpperCase() !== 'GET')) {
                ajaxOpts.contentType = contentType || 'application/json; charset=utf-8';
                ajaxOpts.data = JSON.stringify(requestedIds);
              }
              return $.ajax(ajaxOpts)
                .done(function(data) {
                  var i, requesterIdx, requesterIdsAsDict, models, privateCollection,
                      requesterLength, requesters, model,
                      requestedIdsLength = requestedIds.length,
                      setOptions = loadWrapperOptions.setOptions;
                  collection.set(collection.parse(data), setOptions);
  • ¶

    Set respective collection’s models for requested ids only.

                  requesters = collection.getRequesters();
                  requesterLength = requesters.length;
  • ¶

    For each requester…

                  for (requesterIdx = 0; requesterIdx < requesterLength; requesterIdx++) {
                    requesterIdsAsDict = collection.getRequesterIdsAsDictionary(requesters[requesterIdx]);
                    models = [];
  • ¶

    … now let’s iterate over the ids that were fetched …

                    for (i = 0; i < requestedIdsLength; i++) {
  • ¶

    if the id that the requester cares about matches that model whose id was just fetched…

                      if (requesterIdsAsDict[requestedIds[i]]) {
                        model = collection.get(requestedIds[i]);
  • ¶

    if the model was removed, no worries, the parent won’t attempt to update the child on that one.

                        if (model) {
                          models.push(model);
                        }
                      }
                    }
                    privateCollection = collection.knownPrivateCollections[requesters[requesterIdx]];
  • ¶

    a fetch by the parent will not remove a model in a requester collection that wasn’t fetched with this call

                    if (privateCollection) {
                      privateCollection.set(models, {remove: false});
                    }
                  }
                });
            }, fetchByIdsOptions)
              .always(function() {
  • ¶

    This happens once the promise is resolved, and removes the pending promise for that id.

  • ¶

    Track that the fetch was completed so we don’t add a dead promise (since this is what cleans up completed promises).

                fetchComplete = true;
  • ¶

    Note that an id may have multiple promises (if fetch was called multiple times and then a pull with the same id). We want to wait for all of them to complete, this tracks them separately and removes the in-flight promises once they are done.

                _.each(requestedIds, function(requestedId) {
                  if (collection.idPromises) {
                    var existingPromisesForId = collection.idPromises[requestedId];
                    var remainingPromisesForId = _.without(existingPromisesForId, fetchPromise);
                    if (_.isEmpty(remainingPromisesForId)) {
                      delete collection.idPromises[requestedId];
                    } else {
                      collection.idPromises[requestedId] = remainingPromisesForId;
                    }
                  }
                });
              });
  • ¶

    Track the promises associated with each id so we know when that id has completed fetching. Multiple simultaneous pulls will generate multiple promises for a shared id. Pulls will not generate new promises for the given ids (if there are ids being fetched

            if (!fetchComplete) { // fixes the sync case (mainly during tests when mockjax is used).
              _.each(requestedIds, function(requestedId) {
                var existingPromises = collection.idPromises[requestedId];
                if (!existingPromises) {
                  existingPromises = [];
                  collection.idPromises[requestedId] = existingPromises;
                }
    
                existingPromises.push(fetchPromise);
              });
            }
    
            return fetchPromise;
          };
        };
    
        return /** @lends cacheMixin */ {
          /**
           * The constructor constructor / initialize method for collections.
           * Allocate new memory for the local references if they
           * were null when this method was called.
           *
           * @param {Object} [options] - optional options object
           * @param   [options.fetchHttpAction='POST'] {string} http action used to get objects by ids
           * @param   [options.getByIdsUrl='/ids'] {string} path appended to collection.url to get objects by a list of ids
           * @param   {boolean} [options.fetchUsingTrackedIds=true] if set to false, cache.fetch() will not pass to fetchByIds with current tracked ids
           *                                                       but will rather call the default fetch method.
           */
          constructor: function(models, options) {
            options = options || {};
            base.call(this, models, options);
            this.isRequester = options.isRequester;
            this.parentInstance = options.parentInstance;
            if (!this.isRequester) {
              this.requestMap = {};
              this.collectionTrackedIds = [];
              this.knownPrivateCollections = {};
              this.idPromises = {};
              var cacheDefaults = _.defaults(
                _.pick(options, 'getByIdsUrl', 'fetchHttpAction', 'fetchUsingTrackedIds'),
                _.pick(this, 'getByIdsUrl', 'fetchHttpAction', 'fetchUsingTrackedIds'),
                {
                  getByIdsUrl: '/ids',
                  fetchHttpAction: 'GET',
                  fetchUsingTrackedIds: true
                }
              );
              _.extend(this, cacheDefaults);
              cacheMixin(this);
            } else {
              this.trackedIds = [];
              this.listenTo(this.parentInstance, 'load-begin', function() {
                this.trigger('cache-load-begin');
              });
              this.listenTo(this.parentInstance, 'load-complete', function() {
                this.trigger('cache-load-complete');
              });
            }
          },
        };
      };
    
      return mixin;
    }));