(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; } })();