d2-ui
Version:
453 lines (418 loc) • 17.6 kB
JavaScript
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule OrderedMap
*/
'use strict';
var assign = require('./Object.assign');
var invariant = require('fbjs/lib/invariant');
var PREFIX = 'key:';
/**
* Utility to extract a backing object from an initialization `Array`, allowing
* the caller to assist in resolving the unique ID for each entry via the
* `keyExtractor` callback. The `keyExtractor` must extract non-empty strings or
* numbers.
* @param {Array<Object!>} arr Array of items.
* @param {function} keyExtractor Extracts a unique key from each item.
* @return {Object} Map from unique key to originating value that the key was
* extracted from.
* @throws Exception if the initialization array has duplicate extracted keys.
*/
function extractObjectFromArray(arr, keyExtractor) {
var normalizedObj = {};
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
var key = keyExtractor(item);
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
!!(normalizedKey in normalizedObj) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap: IDs returned by the key extraction function must be unique.') : invariant(false) : undefined;
normalizedObj[normalizedKey] = item;
}
return normalizedObj;
}
/**
* Utility class for mappings with ordering. This class is to be used in an
* immutable manner. A `OrderedMap` is very much like the native JavaScript
* object, where keys map to values via the `get()` function. Also, like the
* native JavaScript object, there is an ordering associated with the mapping.
* This class is helpful because it eliminates many of the pitfalls that come
* with the native JavaScript ordered mappings. Specifically, there are
* inconsistencies with numeric keys in some JavaScript implementations
* (enumeration ordering). This class protects against those pitfalls and
* provides functional utilities for dealing with these `OrderedMap`s.
*
* - TODO:
* - orderedMergeExclusive: Merges mutually exclusive `OrderedMap`s.
* - mapReverse().
*
* @class {OrderedMap}
* @constructor {OrderedMap}
* @param {Object} normalizedObj Object that is known to be a defensive copy of
* caller supplied data. We require a defensive copy to guard against callers
* mutating. It is also assumed that the keys of `normalizedObj` have been
* normalized and do not contain any numeric-appearing strings.
* @param {number} computedLength The precomputed length of `_normalizedObj`
* keys.
* @private
*/
function OrderedMapImpl(normalizedObj, computedLength) {
this._normalizedObj = normalizedObj;
this._computedPositions = null;
this.length = computedLength;
}
/**
* Validates a "public" key - that is, one that the public facing API supplies.
* The key is then normalized for internal storage. In order to be considered
* valid, all keys must be non-empty, defined, non-null strings or numbers.
*
* @param {string?} key Validates that the key is suitable for use in a
* `OrderedMap`.
* @throws Error if key is not appropriate for use in `OrderedMap`.
*/
function assertValidPublicKey(key) {
!(key !== '' && (typeof key === 'string' || typeof key === 'number')) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap: Key must be non-empty, non-null string or number.') : invariant(false) : undefined;
}
/**
* Validates that arguments to range operations are within the correct limits.
*
* @param {number} start Start of range.
* @param {number} length Length of range.
* @param {number} actualLen Actual length of range that should not be
* exceeded.
* @throws Error if range arguments are out of bounds.
*/
function assertValidRangeIndices(start, length, actualLen) {
!(typeof start === 'number' && typeof length === 'number' && length >= 0 && start >= 0 && start + length <= actualLen) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap: `mapRange` and `forEachRange` expect non-negative start and ' + 'length arguments within the bounds of the instance.') : invariant(false) : undefined;
}
/**
* Merges two "normalized" objects (objects who's key have been normalized) into
* a `OrderedMap`.
*
* @param {Object} a Object of key value pairs.
* @param {Object} b Object of key value pairs.
* @return {OrderedMap} new `OrderedMap` that results in merging `a` and `b`.
*/
function _fromNormalizedObjects(a, b) {
// Second optional, both must be plain JavaScript objects.
!(a && a.constructor === Object && (!b || b.constructor === Object)) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap: Corrupted instance of OrderedMap detected.') : invariant(false) : undefined;
var newSet = {};
var length = 0;
var key;
for (key in a) {
if (a.hasOwnProperty(key)) {
newSet[key] = a[key];
length++;
}
}
for (key in b) {
if (b.hasOwnProperty(key)) {
// Increment length if not already added via first object (a)
if (!(key in newSet)) {
length++;
}
newSet[key] = b[key];
}
}
return new OrderedMapImpl(newSet, length);
}
/**
* Methods for `OrderedMap` instances.
*
* @lends OrderedMap.prototype
* TODO: Make this data structure lazy, unify with LazyArray.
* TODO: Unify this with ImmutableObject - it is to be used immutably.
* TODO: If so, consider providing `fromObject` API.
* TODO: Create faster implementation of merging/mapping from original Array,
* without having to first create an object - simply for the sake of merging.
*/
var OrderedMapMethods = {
/**
* Returns whether or not a given key is present in the map.
*
* @param {string} key Valid string key to lookup membership for.
* @return {boolean} Whether or not `key` is a member of the map.
* @throws Error if provided known invalid key.
*/
has: function (key) {
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
return normalizedKey in this._normalizedObj;
},
/**
* Returns the object for a given key, or `undefined` if not present. To
* distinguish an undefined entry vs not being in the set, use `has()`.
*
* @param {string} key String key to lookup the value for.
* @return {Object?} Object at key `key`, or undefined if not in map.
* @throws Error if provided known invalid key.
*/
get: function (key) {
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
return this.has(key) ? this._normalizedObj[normalizedKey] : undefined;
},
/**
* Merges, appending new keys to the end of the ordering. Keys in `orderedMap`
* that are redundant with `this`, maintain the same ordering index that they
* had in `this`. This is how standard JavaScript object merging would work.
* If you wish to prepend a `OrderedMap` to the beginning of another
* `OrderedMap` then simply reverse the order of operation. This is the analog
* to `merge(x, y)`.
*
* @param {OrderedMap} orderedMap OrderedMap to merge onto the end.
* @return {OrderedMap} New OrderedMap that represents the result of the
* merge.
*/
merge: function (orderedMap) {
!(orderedMap instanceof OrderedMapImpl) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap.merge(...): Expected an OrderedMap instance.') : invariant(false) : undefined;
return _fromNormalizedObjects(this._normalizedObj, orderedMap._normalizedObj);
},
/**
* Functional map API. Returns a new `OrderedMap`.
*
* @param {Function} cb Callback to invoke for each item.
* @param {Object?=} context Context to invoke callback from.
* @return {OrderedMap} OrderedMap that results from mapping.
*/
map: function (cb, context) {
return this.mapRange(cb, 0, this.length, context);
},
/**
* The callback `cb` is invoked with the arguments (item, key,
* indexInOriginal).
*
* @param {Function} cb Determines result for each item.
* @param {number} start Start index of map range.
* @param {end} length End index of map range.
* @param {*!?} context Context of callback invocation.
* @return {OrderedMap} OrderedMap resulting from mapping the range.
*/
mapRange: function (cb, start, length, context) {
var thisSet = this._normalizedObj;
var newSet = {};
var i = 0;
assertValidRangeIndices(start, length, this.length);
var end = start + length - 1;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
if (i >= start) {
if (i > end) {
break;
}
var item = thisSet[key];
newSet[key] = cb.call(context, item, key.substr(PREFIX.length), i);
}
i++;
}
}
return new OrderedMapImpl(newSet, length);
},
/**
* Function filter API. Returns new `OrderedMap`.
*
* @param {Function} cb Callback to invoke for each item.
* @param {Object?=} context Context to invoke callback from.
* @return {OrderedMap} OrderedMap that results from filtering.
*/
filter: function (cb, context) {
return this.filterRange(cb, 0, this.length, context);
},
/**
* The callback `cb` is invoked with the arguments (item, key,
* indexInOriginal).
*
* @param {Function} cb Returns true if item should be in result.
* @param {number} start Start index of filter range.
* @param {number} length End index of map range.
* @param {*!?} context Context of callback invocation.
* @return {OrderedMap} OrderedMap resulting from filtering the range.
*/
filterRange: function (cb, start, length, context) {
var newSet = {};
var newSetLength = 0;
this.forEachRange(function (item, key, originalIndex) {
if (cb.call(context, item, key, originalIndex)) {
var normalizedKey = PREFIX + key;
newSet[normalizedKey] = item;
newSetLength++;
}
}, start, length);
return new OrderedMapImpl(newSet, newSetLength);
},
forEach: function (cb, context) {
this.forEachRange(cb, 0, this.length, context);
},
forEachRange: function (cb, start, length, context) {
assertValidRangeIndices(start, length, this.length);
var thisSet = this._normalizedObj;
var i = 0;
var end = start + length - 1;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
if (i >= start) {
if (i > end) {
break;
}
var item = thisSet[key];
cb.call(context, item, key.substr(PREFIX.length), i);
}
i++;
}
}
},
/**
* Even though `mapRange`/`forEachKeyRange` allow zero length mappings, we'll
* impose an additional restriction here that the length of mapping be greater
* than zero - the only reason is that there are many ways to express length
* zero in terms of two keys and that is confusing.
*/
mapKeyRange: function (cb, startKey, endKey, context) {
var startIndex = this.indexOfKey(startKey);
var endIndex = this.indexOfKey(endKey);
!(startIndex !== undefined && endIndex !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'mapKeyRange must be given keys that are present.') : invariant(false) : undefined;
!(endIndex >= startIndex) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap.mapKeyRange(...): `endKey` must not come before `startIndex`.') : invariant(false) : undefined;
return this.mapRange(cb, startIndex, endIndex - startIndex + 1, context);
},
forEachKeyRange: function (cb, startKey, endKey, context) {
var startIndex = this.indexOfKey(startKey);
var endIndex = this.indexOfKey(endKey);
!(startIndex !== undefined && endIndex !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'forEachKeyRange must be given keys that are present.') : invariant(false) : undefined;
!(endIndex >= startIndex) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap.forEachKeyRange(...): `endKey` must not come before ' + '`startIndex`.') : invariant(false) : undefined;
this.forEachRange(cb, startIndex, endIndex - startIndex + 1, context);
},
/**
* @param {number} pos Index to search for key at.
* @return {string|undefined} Either the key at index `pos` or undefined if
* not in map.
*/
keyAtIndex: function (pos) {
var computedPositions = this._getOrComputePositions();
var keyAtPos = computedPositions.keyByIndex[pos];
return keyAtPos ? keyAtPos.substr(PREFIX.length) : undefined;
},
/**
* @param {string} key String key from which to find the next key.
* @return {string|undefined} Either the next key, or undefined if there is no
* next key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
keyAfter: function (key) {
return this.nthKeyAfter(key, 1);
},
/**
* @param {string} key String key from which to find the preceding key.
* @return {string|undefined} Either the preceding key, or undefined if there
* is no preceding.key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
keyBefore: function (key) {
return this.nthKeyBefore(key, 1);
},
/**
* @param {string} key String key from which to find a following key.
* @param {number} n Distance to scan forward after `key`.
* @return {string|undefined} Either the nth key after `key`, or undefined if
* there is no next key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
nthKeyAfter: function (key, n) {
var curIndex = this.indexOfKey(key);
!(curIndex !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap.nthKeyAfter: The key `%s` does not exist in this instance.', key) : invariant(false) : undefined;
return this.keyAtIndex(curIndex + n);
},
/**
* @param {string} key String key from which to find a preceding key.
* @param {number} n Distance to scan backwards before `key`.
* @return {string|undefined} Either the nth key before `key`, or undefined if
* there is no previous key.
* @throws Error if `key` is not in this `OrderedMap`.
*/
nthKeyBefore: function (key, n) {
return this.nthKeyAfter(key, -n);
},
/**
* @param {string} key Key to find the index of.
* @return {number|undefined} Index of the provided key, or `undefined` if the
* key is not found.
*/
indexOfKey: function (key) {
assertValidPublicKey(key);
var normalizedKey = PREFIX + key;
var computedPositions = this._getOrComputePositions();
var computedPosition = computedPositions.indexByKey[normalizedKey];
// Just writing it this way to make it clear this is intentional.
return computedPosition === undefined ? undefined : computedPosition;
},
/**
* @return {Array} An ordered array of this object's values.
*/
toArray: function () {
var result = [];
var thisSet = this._normalizedObj;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
result.push(thisSet[key]);
}
}
return result;
},
/**
* Finds the key at a given position, or indicates via `undefined` that that
* position does not exist in the `OrderedMap`. It is appropriate to return
* undefined, indicating that the key doesn't exist in the `OrderedMap`
* because `undefined` is not ever a valid `OrderedMap` key.
*
* @private
* @return {string?} Name of the item at position `pos`, or `undefined` if
* there is no item at that position.
*/
_getOrComputePositions: function () {
// TODO: Entertain computing this at construction time in some less
// performance critical paths.
var computedPositions = this._computedPositions;
if (!computedPositions) {
this._computePositions();
}
return this._computedPositions;
},
/**
* Precomputes the index/key mapping for future lookup. Since `OrderedMap`s
* are immutable, there is only ever a need to perform this once.
* @private
*/
_computePositions: function () {
this._computedPositions = {
keyByIndex: {},
indexByKey: {}
};
var keyByIndex = this._computedPositions.keyByIndex;
var indexByKey = this._computedPositions.indexByKey;
var index = 0;
var thisSet = this._normalizedObj;
for (var key in thisSet) {
if (thisSet.hasOwnProperty(key)) {
keyByIndex[index] = key;
indexByKey[key] = index;
index++;
}
}
}
};
assign(OrderedMapImpl.prototype, OrderedMapMethods);
var OrderedMap = {
from: function (orderedMap) {
!(orderedMap instanceof OrderedMapImpl) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap.from(...): Expected an OrderedMap instance.') : invariant(false) : undefined;
return _fromNormalizedObjects(orderedMap._normalizedObj, null);
},
fromArray: function (arr, keyExtractor) {
!Array.isArray(arr) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap.fromArray(...): First argument must be an array.') : invariant(false) : undefined;
!(typeof keyExtractor === 'function') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'OrderedMap.fromArray(...): Second argument must be a function used ' + 'to determine the unique key for each entry.') : invariant(false) : undefined;
return new OrderedMapImpl(extractObjectFromArray(arr, keyExtractor), arr.length);
}
};
module.exports = OrderedMap;