mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-02 07:05:24 +00:00
393 lines
12 KiB
JavaScript
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;
|
||
|
}
|
||
|
})();
|