towry
9/17/2015 - 2:50 AM

datastore.js

var assign = require('object-assign');

/**
 * DataStore
 * http://gitcafe.com/towry
 *
 * Provide an index persistent data store.
 * The data is a list, that will be added with new items, 
 * or delete item from it, but the original index used by
 * the react component must be persistence, so that we can
 * get the data from data store with the index after the list
 * changed.
 *
 * DO NOT USE THIS DIRECTLY.
 */

function DataStore () {
  this._data = [];
  this._removedIndex = [];
  this._subscribers = [];
  this._sequence = [];

  this._rents = [];

  this._deleted = [];

  this._cache = [];
}

module.exports = DataStore;

/**
 * @api public
 */
DataStore.prototype.setData = function (data) {
  if (this._data.length) {
    return;
  }

  if (Object.prototype.toString.call(data) !== '[object Array]') {
    typeof console !== 'undefined' && console.warn && console.warn("argument is not an array");
    return;
  }

  // make a copy of it
  this._data = data.slice();
  for (var i = 0; i < data.length; i++) {
    this._sequence.push(i);
  }
}

/**
 * @api public
 */
DataStore.prototype.replaceData = function (data) {
  this._data = data;
  this._sequence = [];
  for (var i = 0; i < data.length; i++) {
    this._sequence.push(i);
  }
}

/**
 * @api public
 */
DataStore.prototype.all = function () {
  if (this._cache.length) {
    return this._cache;
  }

  var all = [];
  for (var i = 0; i < this._sequence.length; i++) {
    all.push(this._data[this._sequence[i]]);
  }

  this._cache = all;

  return all;
}

/**
 * The `cb` is a function that takes three arguments
 * [item, index, persistent index]
 * So, do not use the index as the component's key,
 * because the data may be changed in the futrue and the
 * index will be taken by another component.
 * Use the persistent index.
 * @api public
 */
DataStore.prototype.forEach = function (cb, context) {
  var item;
  var indic = 0;
  for (var i = 0; i < this._sequence.length; i++) {
    item = this._data[this._sequence[i]];
    if (item === null || item === undefined) {
      continue;
    }
    cb.call(context, this._data[this._sequence[i]], indic, this._sequence[i]);
    indic++;
  }
}

/**
 * @api public
 */
DataStore.prototype.isEmpty = function () {
  return this._sequence.length === 0;
}

/**
 * Return new index
 * When you want to get an index for later use
 * @api public
 */
DataStore.prototype.rent = function () {
  var id;
  if (this._removedIndex.length) {
    id = this._removedIndex.shift();
  } else {
    id = this._data.length;
    this._data.push(null);
  }

  this._rents.push(id);

  return id;
}

/**
 * When datastore change, inform the subscribers
 * @api public
 */
DataStore.prototype.subscribe = function (cb) {
  if (this._subscribers.indexOf(cb) !== -1) {
    return;
  }

  return this._subscribers.push(cb);
}

/**
 * @api private
 */
DataStore.prototype.update = function (a) {
  this._cache = [];
  
  var cbs = this._subscribers, cb;

  for (var i = 0, l = cbs.length; i < l; i++) {
    cb = cbs[i];
    cb(a);
  }
}

/**
 * Add an item to datastore
 * @api public
 */
DataStore.prototype.add = function (item, index) {
  var indexType;
  if ((indexType = Object.prototype.toString.call(index)) === '[object Undefined]') {
    throw new Error("When adding an item to store, please provide an index or null value");
  }

  index = indexType == '[object Number]' ? index : this.getAvailableIndex();
  this._data[index] = item;

  var idx = this._rents.indexOf(index);
  if (idx >= 0) {
    this._rents.splice(idx, 1);
  }

  this._sequence.push(index);

  this.update({
    index: index,
    type: 'add',
  });
}

DataStore.prototype.clear = function () {
  var ret = this._deleted;
  this._deleted = [];
  return ret;
}

DataStore.prototype.hasDestroy = function () {
  return this._deleted.length !== 0;
}

/**
 * Remove an item from datastore
 * @api public
 */
DataStore.prototype.remove = function (index, cb) {
  if (index >= this._data.length) {
    return;
  }

  /* TODO
   */
  this._deleted.push(this._data[index]);
  this._deleted[this._deleted.length - 1]._destroy = 1;
  /** */

  this._data[index] = null;

  var idx = this._rents.indexOf(index);
  if (idx >= 0) {
    this._rents.splice(idx, 1);
  }
  idx = this._sequence.indexOf(index);
  if (idx >= 0) {
    this._sequence.splice(idx, 1);
  }

  this.addRemoveIndex(index);

  this.update({
    index: index,
    type: 'remove',
  });

  cb();
}

/**
 * @api public
 */
DataStore.prototype.put = function (index, value) {
  if (index >= this._data.length) {
    return;
  }

  this.removeIndexFromRemove(index);
  var old = this._data[index];

  if (Object.prototype.toString.call(value) === '[object Object]') {
    this._data[index] = assign({}, old, value);
  } else {
    this._data[index] = value;
  }

  var idx = this._sequence.indexOf(index);
  if (idx < 0) {
    this._sequence.push(index);
  }

  idx = this._rents.indexOf(index);
  if (idx >= 0) {
    this._rents.splice(idx, 1);
  }

  this.update({
    index: index,
    type: 'put',
  })
}

/**
 * @api private
 */
DataStore.prototype.removeIndexFromRemove = function (index) {
  var idx = this._removedIndex.indexOf(index);
  if (idx < 0) return;

  this._removedIndex.splice(idx, 1);
}

/**
 * @api private
 */
DataStore.prototype.addRemoveIndex = function (index) {
  var idx = this._removedIndex.indexOf(index);
  if (idx >= 0) {
    return;
  } else {
    this._removedIndex.push(index);
  }
}

/**
 * @api private
 */
DataStore.prototype.getAvailableIndex = function () {
  if (this._removedIndex.length) {
    return this._removedIndex.pop();
  } else {
    return this._data.length;
  }
}

/**
 * @api public
 */
DataStore.prototype.destroy = function () {
  this._subscribers = [];
  this._data = [];
  this._removedIndex = [];
}

/**
 * @api public
 */
DataStore.createStore = function (child, a) {
  if (typeof child !== 'function') {
    throw new TypeError("argument is not a function");
  }
  
  return new child(a);
}