Source: RepeatableCollection.js

define(
  [
    'backbone',
    'underscore',
    'jquery'
  ], function(
    Backbone,
    _,
    $
  ) {

  'use strict';

  /**
   * Wrap a collection and repeat its contents infinitely.
   * Will generate unique ids for the repeated models to avoid duplication.
   *
   * ```js
   * var sourceCollection = new Backbone.Collection( ... );
   * var repeatableCollection = new RepeatableCollection([], {
   *   collection: sourceCollection
   * });
   * ```
   */
  var RepeatableCollection = Backbone.Collection.extend({

    initialize: function(models, options) {
      this.options = _.defaults({}, options, {per_page: 20});
      this.source = this.options.collection;
      this.repeating = false;
      this.currentIndex = 0;

      delete this.options.collection;
    },

    /**
     * Fetch models. If no more models are available, the current page will be filled by replicas of the original collection's items.
     */
    fetch: function(options) {
      // fetch only one page at a time
      if (this._fetching) {
        return this._fetching;
      }

      options = _.extend(this.options, options);

      var promise = $.Deferred();
      var rC = this;

      ensureItems(this, options).done(function() {

        var models = duplicate(rC.source.models.slice(rC.currentIndex, rC.currentIndex + options.per_page));
        var diff = rC.currentIndex + options.per_page - rC.source.length;

        if (diff > 0) {
          // end of source data
          models = models.concat(duplicate(rC.source.models.slice(0, diff)));
          rC.currentIndex = diff;
          rC.repeating = true;
        } else {
          rC.currentIndex += options.per_page;
        }

        rC.add(models);
        promise.resolve(models);
        if (options.success) { options.success(rC, models, options); }
        rC.trigger('sync', rC, models, options);

      });

      var fetching = this._fetching = promise;
      this._fetching.done(function() {
        delete rC._fetching;
      });

      return fetching;
    }

  });

  return RepeatableCollection;


  // helper functions

  function ensureItems(rC, options, promise) {
    if (!promise) { promise = new $.Deferred(); }

    if (rC.repeating || rC.source.length >= rC.currentIndex + options.per_page) {
      promise.resolve();
    } else {
      var length = rC.source.length;
      rC.source.fetch({reset: false}).done(function() {
        if (rC.source.length === length) {
          rC.repeating = true;
        }
        ensureItems(rC, options, promise);
      });
    }

    return promise;
  }

  function duplicate(models) {
    return _.map(models, function(model) {
      model = model.toJSON();
      model.id = _.uniqueId(model.id + '_');
      return model;
    });
  }

});