Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[indexedArray] es6-ify #11800

Merged
merged 1 commit into from
May 15, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 143 additions & 131 deletions src/ui/public/indexed_array/indexed_array.js
Original file line number Diff line number Diff line change
@@ -1,156 +1,168 @@
import _ from 'lodash';
import { inflector } from 'ui/indexed_array/inflector';

import { inflector } from './inflector';

const pathGetter = _(_.get).rearg(1, 0).ary(2);
const inflectIndex = inflector('by');
const inflectOrder = inflector('in', 'Order');

const CLEAR_CACHE = {};
const OPT_NAMES = IndexedArray.OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable'];
const OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable'];

/**
* Generic extension of Array class, which will index (and reindex) the
* objects it contains based on their properties.
* Generic extension of Array class, which will index (and reindex) the
* objects it contains based on their properties.
*
* @class IndexedArray
* @module utils
* @constructor
* @param {object} [config] - describes the properties of this registry object
* @param {string[]} [config.index] - a list of props/paths that should be used to index the docs.
* @param {string[]} [config.group] - a list of keys/paths to group docs by.
* @param {string[]} [config.order] - a list of keys/paths to order the keys by.
* @param {object[]} [config.initialSet] - the initial dataset the IndexedArray should contain.
* @param {boolean} [config.immutable] - a flag that hints to people reading the implementation
* that this IndexedArray should not be modified. It's modification
* methods are also removed
* @param {Object} config describes the properties of this registry object
* @param {Array<string>} [config.index] a list of props/paths that should be used to index the docs.
* @param {Array<string>} [config.group] a list of keys/paths to group docs by.
* @param {Array<string>} [config.order] a list of keys/paths to order the keys by.
* @param {Array<any>} [config.initialSet] the initial dataset the IndexedArray should contain.
* @param {boolean} [config.immutable] a flag that hints to people reading the implementation that this IndexedArray
* should not be modified
*/
_.class(IndexedArray).inherits(Array);
export function IndexedArray(config) {
IndexedArray.Super.call(this);

// just to remind future us that this list is important
config = _.pick(config || {}, OPT_NAMES);
export class IndexedArray {
static OPT_NAMES = OPT_NAMES

this.raw = [];
constructor(config) {
config = _.pick(config || {}, OPT_NAMES);

// setup indices
this._indexNames = _.union(
this._setupIndices(config.group, inflectIndex, _.organizeBy),
this._setupIndices(config.index, inflectIndex, _.indexBy),
this._setupIndices(config.order, inflectOrder, _.sortBy)
);
// use defineProperty so that value can't be changed
Object.defineProperty(this, 'raw', { value: [] });

if (config.initialSet) {
this.push.apply(this, config.initialSet);
}
this._indexNames = _.union(
this._setupIndex(config.group, inflectIndex, _.organizeBy),
this._setupIndex(config.index, inflectIndex, _.indexBy),
this._setupIndex(config.order, inflectOrder, _.sortBy)
);

if (config.immutable) {
// just a hint, bugs caused by updates not propogating would be very
// very very hard to track down
this.push = this.splice = undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was loosing this intentional ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they are now implemented as methods and throw descriptive errors when immutable is turned on (it also covers all of the mutable methods, not just push and splice).

}
}
if (config.initialSet) {
this.push.apply(this, config.initialSet);
}

/**
* Create indices for a group of object properties. getters and setters are used to
* read and control the indices.
*
* @param {string[]} props - the properties that should be used to index docs
* @param {function} inflect - a function that will be called with a property name, and
* creates the public property at which the index will be exposed
* @param {function} op - the function that will be used to create the indices, it is passed
* the raw representaion of the registry, and a getter for reading the
* right prop
*
* @returns {string[]} - the public keys of all indices created
*/
IndexedArray.prototype._setupIndices = function (props, inflect, op) {
// shortcut for empty props
if (!props || props.length === 0) return;

const self = this;
return props.map(function (prop) {

const from = pathGetter.partial(prop).value();
const to = inflect(prop);
let cache;

Object.defineProperty(self, to, {
enumerable: false,
configurable: false,

set: function (val) {
// can't set any value other than the CLEAR_CACHE constant
if (val === CLEAR_CACHE) {
cache = false;
} else {
throw new TypeError(to + ' can not be set, it is a computed index of values');
}
},
get: function () {
return cache || (cache = op(self.raw, from));
}
});

return to;
});

};
Object.defineProperty(this, 'immutable', { value: !!config.immutable });
}

/**
* (Re)run index/group/order procedures to create indices of
* sub-objects.
*
* @return {undefined}
*/
IndexedArray.prototype._clearIndices = function () {
const self = this;
self._indexNames.forEach(function (name) {
self[name] = CLEAR_CACHE;
});
};
/**
* Remove items from this based on a predicate
* @param {Function|Object|string} predicate - the predicate used to decide what is removed
* @return {array} - the removed data
*/
remove(predicate) {
this._assertMutable('remove');
const out = _.remove(this, predicate);
_.remove(this.raw, predicate);
this._clearIndices();
return out;
}

/**
* Copy all array methods which have side-effects, and wrap them
* in a function that will reindex after each call, as well
* as duplex the operation to the .raw version of the IndexedArray.
*
* @param {[type]} method [description]
* @return {[type]} [description]
*/
'pop push shift splice unshift reverse'.split(' ').forEach(function (method) {
const orig = Array.prototype[method];
/**
* provide a hook for the JSON serializer
* @return {array} - a plain, vanilla array with our same data
*/
toJSON() {
return this.raw;
}

IndexedArray.prototype[method] = function (/* args... */) {
// call the original method with this context
orig.apply(this, arguments);
// wrappers for mutable Array methods
copyWithin(...args) { return this._mutation('copyWithin', args); }
fill(...args) { return this._mutation('fill', args); }
pop(...args) { return this._mutation('pop', args); }
push(...args) { return this._mutation('push', args); }
reverse(...args) { return this._mutation('reverse', args); }
shift(...args) { return this._mutation('shift', args); }
sort(...args) { return this._mutation('sort', args); }
splice(...args) { return this._mutation('splice', args); }
unshift(...args) { return this._mutation('unshift', args); }

/**
* If this instance of IndexedArray is not mutable, throw an error
* @private
* @param {String} methodName - user facing method name, for error message
* @return {undefined}
*/
_assertMutable(methodName) {
if (this.immutable) {
throw new Error(`${methodName}() is not allowed on immutable IndexedArray instances`);
}
}

// run the indexers
/**
* Execute some mutable method from the Array prototype
* on the IndexedArray and this.raw
*
* @private
* @param {string} methodName
* @param {Array<any>} args
* @return {any}
*/
_mutation(methodName, args) {
this._assertMutable(methodName);
super[methodName].apply(this, args);
this._clearIndices();
return super[methodName].apply(this.raw, args);
}

// call the original method on our "raw" array, and return the result(s)
return orig.apply(this.raw, arguments);
};
});
/**
* Create indices for a group of object properties. getters and setters are used to
* read and control the indices.
* @private
* @param {string[]} props - the properties that should be used to index docs
* @param {function} inflect - a function that will be called with a property name, and
* creates the public property at which the index will be exposed
* @param {function} op - the function that will be used to create the indices, it is passed
* the raw representaion of the registry, and a getter for reading the
* right prop
*
* @returns {string[]} - the public keys of all indices created
*/
_setupIndex(props, inflect, op) {
// shortcut for empty props
if (!props || props.length === 0) return;

return props.map(prop => {
const indexName = inflect(prop);
const getIndexValueFromItem = pathGetter.partial(prop).value();
let cache;

Object.defineProperty(this, indexName, {
enumerable: false,
configurable: false,

set: val => {
// can't set any value other than the CLEAR_CACHE constant
if (val === CLEAR_CACHE) {
cache = false;
} else {
throw new TypeError(indexName + ' can not be set, it is a computed index of values');
}
},
get: () => {
if (!cache) {
cache = op(this.raw, getIndexValueFromItem);
}

return cache;
}
});

/**
* Remove items from this based on a predicate
* @param {function|object|string} predicate - the predicate used to decide what is removed
* @param {object} context - this binding for predicate
* @return {array} - the removed data
*/
IndexedArray.prototype.remove = function (predicate, context) {
const out = _.remove(this, predicate, context);
_.remove(this.raw, predicate, context);
this._clearIndices();
return out;
};
return indexName;
});
}

/**
* provide a hook for the JSON serializer
* @return {array} - a plain, vanilla array with our same data
*/
IndexedArray.prototype.toJSON = function () {
return this.raw;
};
/**
* Clear cached index/group/order caches so they will be recreated
* on next access
* @private
* @return {undefined}
*/
_clearIndices() {
this._indexNames.forEach(name => {
this[name] = CLEAR_CACHE;
});
}
}

// using traditional `extends Array` syntax doesn't work with babel
// See https://babeljs.io/docs/usage/caveats/
Object.setPrototypeOf(IndexedArray.prototype, Array.prototype);