gecko-dev/devtools/client/shared/vendor/seamless-immutable.js

393 lines
12 KiB
JavaScript

(function(){
"use strict";
function addPropertyTo(target, methodName, value) {
Object.defineProperty(target, methodName, {
enumerable: false,
configurable: false,
writable: false,
value: value
});
}
function banProperty(target, methodName) {
addPropertyTo(target, methodName, function() {
throw new ImmutableError("The " + methodName +
" method cannot be invoked on an Immutable data structure.");
});
}
var immutabilityTag = "__immutable_invariants_hold";
function addImmutabilityTag(target) {
addPropertyTo(target, immutabilityTag, true);
}
function isImmutable(target) {
if (typeof target === "object") {
return target === null || target.hasOwnProperty(immutabilityTag);
} else {
// In JavaScript, only objects are even potentially mutable.
// strings, numbers, null, and undefined are all naturally immutable.
return true;
}
}
function isMergableObject(target) {
return target !== null && typeof target === "object" && !(target instanceof Array) && !(target instanceof Date);
}
var mutatingObjectMethods = [
"setPrototypeOf"
];
var nonMutatingObjectMethods = [
"keys"
];
var mutatingArrayMethods = mutatingObjectMethods.concat([
"push", "pop", "sort", "splice", "shift", "unshift", "reverse"
]);
var nonMutatingArrayMethods = nonMutatingObjectMethods.concat([
"map", "filter", "slice", "concat", "reduce", "reduceRight"
]);
function ImmutableError(message) {
var err = new Error(message);
err.__proto__ = ImmutableError;
return err;
}
ImmutableError.prototype = Error.prototype;
function makeImmutable(obj, bannedMethods) {
// Tag it so we can quickly tell it's immutable later.
addImmutabilityTag(obj);
if ("development" === "development") {
// Make all mutating methods throw exceptions.
for (var index in bannedMethods) {
if (bannedMethods.hasOwnProperty(index)) {
banProperty(obj, bannedMethods[index]);
}
}
// Freeze it and return it.
Object.freeze(obj);
}
return obj;
}
function makeMethodReturnImmutable(obj, methodName) {
var currentMethod = obj[methodName];
addPropertyTo(obj, methodName, function() {
return Immutable(currentMethod.apply(obj, arguments));
});
}
function makeImmutableArray(array) {
// Don't change their implementations, but wrap these functions to make sure
// they always return an immutable value.
for (var index in nonMutatingArrayMethods) {
if (nonMutatingArrayMethods.hasOwnProperty(index)) {
var methodName = nonMutatingArrayMethods[index];
makeMethodReturnImmutable(array, methodName);
}
}
addPropertyTo(array, "flatMap", flatMap);
addPropertyTo(array, "asObject", asObject);
addPropertyTo(array, "asMutable", asMutableArray);
for(var i = 0, length = array.length; i < length; i++) {
array[i] = Immutable(array[i]);
}
return makeImmutable(array, mutatingArrayMethods);
}
/**
* Effectively performs a map() over the elements in the array, using the
* provided iterator, except that whenever the iterator returns an array, that
* array's elements are added to the final result instead of the array itself.
*
* @param {function} iterator - The iterator function that will be invoked on each element in the array. It will receive three arguments: the current value, the current index, and the current object.
*/
function flatMap(iterator) {
// Calling .flatMap() with no arguments is a no-op. Don't bother cloning.
if (arguments.length === 0) {
return this;
}
var result = [],
length = this.length,
index;
for (index = 0; index < length; index++) {
var iteratorResult = iterator(this[index], index, this);
if (iteratorResult instanceof Array) {
// Concatenate Array results into the return value we're building up.
result.push.apply(result, iteratorResult);
} else {
// Handle non-Array results the same way map() does.
result.push(iteratorResult);
}
}
return makeImmutableArray(result);
}
/**
* Returns an Immutable copy of the object without the given keys included.
*
* @param {array} keysToRemove - A list of strings representing the keys to exclude in the return value. Instead of providing a single array, this method can also be called by passing multiple strings as separate arguments.
*/
function without(keysToRemove) {
// Calling .without() with no arguments is a no-op. Don't bother cloning.
if (arguments.length === 0) {
return this;
}
// If we weren't given an array, use the arguments list.
if (!(keysToRemove instanceof Array)) {
keysToRemove = Array.prototype.slice.call(arguments);
}
var result = this.instantiateEmptyObject();
for (var key in this) {
if (this.hasOwnProperty(key) && (keysToRemove.indexOf(key) === -1)) {
result[key] = this[key];
}
}
return makeImmutableObject(result,
{instantiateEmptyObject: this.instantiateEmptyObject});
}
function asMutableArray(opts) {
var result = [], i, length;
if(opts && opts.deep) {
for(i = 0, length = this.length; i < length; i++) {
result.push( asDeepMutable(this[i]) );
}
} else {
for(i = 0, length = this.length; i < length; i++) {
result.push(this[i]);
}
}
return result;
}
/**
* Effectively performs a [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) over the elements in the array, expecting that the iterator function
* will return an array of two elements - the first representing a key, the other
* a value. Then returns an Immutable Object constructed of those keys and values.
*
* @param {function} iterator - A function which should return an array of two elements - the first representing the desired key, the other the desired value.
*/
function asObject(iterator) {
// If no iterator was provided, assume the identity function
// (suggesting this array is already a list of key/value pairs.)
if (typeof iterator !== "function") {
iterator = function(value) { return value; };
}
var result = {},
length = this.length,
index;
for (index = 0; index < length; index++) {
var pair = iterator(this[index], index, this),
key = pair[0],
value = pair[1];
result[key] = value;
}
return makeImmutableObject(result);
}
function asDeepMutable(obj) {
if(!obj || !obj.hasOwnProperty(immutabilityTag) || obj instanceof Date) { return obj; }
return obj.asMutable({deep: true});
}
function quickCopy(src, dest) {
for (var key in src) {
if (src.hasOwnProperty(key)) {
dest[key] = src[key];
}
}
return dest;
}
/**
* Returns an Immutable Object containing the properties and values of both
* this object and the provided object, prioritizing the provided object's
* values whenever the same key is present in both objects.
*
* @param {object} other - The other object to merge. Multiple objects can be passed as an array. In such a case, the later an object appears in that list, the higher its priority.
* @param {object} config - Optional config object that contains settings. Supported settings are: {deep: true} for deep merge and {merger: mergerFunc} where mergerFunc is a function
* that takes a property from both objects. If anything is returned it overrides the normal merge behaviour.
*/
function merge(other, config) {
// Calling .merge() with no arguments is a no-op. Don't bother cloning.
if (arguments.length === 0) {
return this;
}
if (other === null || (typeof other !== "object")) {
throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not " + JSON.stringify(other));
}
var anyChanges = false,
result = quickCopy(this, this.instantiateEmptyObject()), // A shallow clone of this object.
receivedArray = (other instanceof Array),
deep = config && config.deep,
merger = config && config.merger,
key;
// Use the given key to extract a value from the given object, then place
// that value in the result object under the same key. If that resulted
// in a change from this object's value at that key, set anyChanges = true.
function addToResult(currentObj, otherObj, key) {
var immutableValue = Immutable(otherObj[key]);
var mergerResult = merger && merger(currentObj[key], immutableValue, config);
if (merger && mergerResult && mergerResult === currentObj[key]) return;
anyChanges = anyChanges ||
mergerResult !== undefined ||
(!currentObj.hasOwnProperty(key) ||
((immutableValue !== currentObj[key]) &&
// Avoid false positives due to (NaN !== NaN) evaluating to true
(immutableValue === immutableValue)));
if (mergerResult) {
result[key] = mergerResult;
} else if (deep && isMergableObject(currentObj[key]) && isMergableObject(immutableValue)) {
result[key] = currentObj[key].merge(immutableValue, config);
} else {
result[key] = immutableValue;
}
}
// Achieve prioritization by overriding previous values that get in the way.
if (!receivedArray) {
// The most common use case: just merge one object into the existing one.
for (key in other) {
if (other.hasOwnProperty(key)) {
addToResult(this, other, key);
}
}
} else {
// We also accept an Array
for (var index=0; index < other.length; index++) {
var otherFromArray = other[index];
for (key in otherFromArray) {
if (otherFromArray.hasOwnProperty(key)) {
addToResult(this, otherFromArray, key);
}
}
}
}
if (anyChanges) {
return makeImmutableObject(result,
{instantiateEmptyObject: this.instantiateEmptyObject});
} else {
return this;
}
}
function asMutableObject(opts) {
var result = this.instantiateEmptyObject(), key;
if(opts && opts.deep) {
for (key in this) {
if (this.hasOwnProperty(key)) {
result[key] = asDeepMutable(this[key]);
}
}
} else {
for (key in this) {
if (this.hasOwnProperty(key)) {
result[key] = this[key];
}
}
}
return result;
}
// Creates plain object to be used for cloning
function instantiatePlainObject() {
return {};
}
// Finalizes an object with immutable methods, freezes it, and returns it.
function makeImmutableObject(obj, options) {
var instantiateEmptyObject =
(options && options.instantiateEmptyObject) ?
options.instantiateEmptyObject : instantiatePlainObject;
addPropertyTo(obj, "merge", merge);
addPropertyTo(obj, "without", without);
addPropertyTo(obj, "asMutable", asMutableObject);
addPropertyTo(obj, "instantiateEmptyObject", instantiateEmptyObject);
return makeImmutable(obj, mutatingObjectMethods);
}
function Immutable(obj, options) {
if (isImmutable(obj)) {
return obj;
} else if (obj instanceof Array) {
return makeImmutableArray(obj.slice());
} else if (obj instanceof Date) {
return makeImmutable(new Date(obj.getTime()));
} else {
// Don't freeze the object we were given; make a clone and use that.
var prototype = options && options.prototype;
var instantiateEmptyObject =
(!prototype || prototype === Object.prototype) ?
instantiatePlainObject : (function() { return Object.create(prototype); });
var clone = instantiateEmptyObject();
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = Immutable(obj[key]);
}
}
return makeImmutableObject(clone,
{instantiateEmptyObject: instantiateEmptyObject});
}
}
// Export the library
Immutable.isImmutable = isImmutable;
Immutable.ImmutableError = ImmutableError;
Object.freeze(Immutable);
/* istanbul ignore if */
if (typeof module === "object") {
module.exports = Immutable;
} else if (typeof exports === "object") {
exports.Immutable = Immutable;
} else if (typeof window === "object") {
window.Immutable = Immutable;
} else if (typeof global === "object") {
global.Immutable = Immutable;
}
})();