imuter
Version:
Immutable data library
481 lines • 15 kB
JavaScript
/// <amd-module name="imuter" />
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeValues = exports.removeValue = exports.writeValues = exports.writeValue = exports.write = exports.array_sort = exports.array_filter = exports.array_map = exports.array_insert = exports.array_slice = exports.array_unshift = exports.array_pop = exports.array_shift = exports.array_push = exports.array_replace = exports.array_exclude = exports.array_remove = exports.array_delete = exports.array_set = exports.object_assign = exports.object_delete = exports.object_set = exports.imuter = void 0;
var tslib_1 = require("tslib");
//Should be determined at compile time to allow tree-shaking
var FREEZING_ENABLED = !(typeof process !== "undefined" && process.env.NODE_ENV === "production");
// eslint-disable-next-line @typescript-eslint/unbound-method
var toString = {}.toString;
function recursiveFreeze(value) {
// Primitives, naturally frozen and already frozen.
// Assume it was deep frozen already as this must stop the recursion.
if (Object.isFrozen(value)) {
return value;
}
// Unfreezable nodes, assuming a numeric nodeType is a DOM Node
if (+value.nodeType) {
return value;
}
switch (toString.call(value)) {
// Unfreezable types via toString()
case "[object Int8Array]":
case "[object Int16Array]":
case "[object Int32Array]":
case "[object Float32Array]":
case "[object Float64Array]":
case "[object Uint8Array]":
case "[object Uint8ClampedArray]":
case "[object Uint16Array]":
case "[object Uint32Array]":
case "[object ArrayBuffer]":
case "[object Blob]":
case "[object DOMWindow]":
case "[object Window]":
case "[object global]":
case "[object XMLHttpRequest]":
return value;
// No need to recurse
case "[object Boolean]":
case "[object Number]":
case "[object String]":
case "[object Date]":
case "[object RegExp]":
return Object.freeze(value);
}
//Freeze before recursing in case of recursive references
Object.freeze(value);
if (Array.isArray(value)) {
for (var _i = 0, value_1 = value; _i < value_1.length; _i++) {
var entry = value_1[_i];
recursiveFreeze(entry);
}
}
else {
for (var key in value) {
recursiveFreeze(value[key]);
}
}
return value;
}
function identity(value) { return value; }
function valueFn(v) { return function () { return v; }; }
var shallowFreeze = FREEZING_ENABLED ? Object.freeze : identity;
var deepFreeze = FREEZING_ENABLED ? recursiveFreeze : identity;
/**
* Freezes the passed object.
*
* NOTE: in production this is a noop/identity function.
*
* @param value the object to freeze
* @returns the passed object, now frozen
*/
exports.imuter = deepFreeze;
var DELETE_VALUE = deepFreeze({});
var REMOVE_VALUE = deepFreeze({});
var REMOVE_VALUE_FN = valueFn(REMOVE_VALUE);
function shallowCloneObject(obj) {
return Object.assign(Object.create(Object.getPrototypeOf(obj)), obj);
}
// Objects
/**
* Sets a property on an object.
*
* If the `obj[prop]` already equals the `value` the existing object is returned.
*
* @param obj the object
* @param prop the property
* @param value the value
* @returns a new (frozen) instance of the object with the property updated
*/
function object_set(obj, prop, value) {
if ((value === DELETE_VALUE || value === REMOVE_VALUE) ? !(prop in obj) : obj[prop] === value) {
return obj;
}
var newObj = shallowCloneObject(obj);
if (value === DELETE_VALUE || value === REMOVE_VALUE) {
delete newObj[prop];
}
else {
FREEZING_ENABLED && deepFreeze(value);
newObj[prop] = value;
}
FREEZING_ENABLED && shallowFreeze(newObj);
return newObj;
}
exports.object_set = object_set;
/**
* `delete`s a property from an object.
*
* If the `obj[prop]` already does not exist the existing object is returned.
*
* @param obj the object
* @param prop the property to delete
* @returns a new (frozen) instance of the object with the property deleted
*/
function object_delete(obj, prop) {
return object_set(obj, prop, DELETE_VALUE);
}
exports.object_delete = object_delete;
function object_assign() {
var sources = [];
for (var _i = 0; _i < arguments.length; _i++) {
sources[_i] = arguments[_i];
}
var newObj = Object.assign.apply(Object, tslib_1.__spreadArray([{}], sources));
FREEZING_ENABLED && deepFreeze(newObj);
return newObj;
}
exports.object_assign = object_assign;
// Arrays
/**
* Assign to an index of the passed array.
*
* If the `array[index]` already equals the `value` the existing array is returned.
*
* @param arr the array
* @param index the index assign to
* @param value the value
* @returns a new (frozen) instance of the array with the specified `index` set to `value`
*/
function array_set(arr, index, value) {
if ((value === DELETE_VALUE || value === REMOVE_VALUE) ? !(index in arr) : arr[index] === value) {
return arr;
}
var newArr = arr.slice();
if (value === DELETE_VALUE) {
delete newArr[index];
}
else if (value === REMOVE_VALUE) {
newArr.splice(index, 1);
}
else {
FREEZING_ENABLED && deepFreeze(value);
newArr[index] = value;
}
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_set = array_set;
/**
* `delete`s an index from the passed array.
*
* If the `array[index]` already does not exist the existing array is returned.
*
* @param arr the array
* @param index the index to delete
* @returns a new (frozen) instance of the array with `index` `delete`ed
*/
function array_delete(arr, index) {
return array_set(arr, index, DELETE_VALUE);
}
exports.array_delete = array_delete;
/**
* Removes entries from an array. Equivelent to the standard `splice`.
*
* If the `array[index]` does not exist the existing array is returned.
*
* @param arr the array
* @param index the index to remove from
* @param deleteCount the number of entries to remove (default: 1)
* @returns a new (frozen) instance of the array with `deleteCount` entries removed at `index`
*/
function array_remove(arr, index, deleteCount) {
if (deleteCount === void 0) { deleteCount = 1; }
if (arr.length <= index || deleteCount === 0) {
return arr;
}
var newArr = arr.slice();
newArr.splice(index, deleteCount);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_remove = array_remove;
function notEqualThis(x) {
return x !== this;
}
/**
* Remove all occurances of a value from an array.
*
* If no occurances exist the existing array is returned.
*
* @param arr the array
* @param value the value to remove from the array
* @returns a new (frozen) instance of the array with `value` removed
*/
function array_exclude(arr, value) {
return array_filter(arr, notEqualThis, value);
}
exports.array_exclude = array_exclude;
/**
* Replace all occurances of a value with a new value.
*
* @param arr the array
* @param oldValue the value to replace
* @param newValue the new value
* @returns a new (frozen) instance of the array with `oldValue` replaced with `newValue`
*/
function array_replace(arr, oldValue, newValue) {
FREEZING_ENABLED && deepFreeze(newValue);
var found = false;
var n = array_map(arr, function (v) { return v === oldValue ? (found = true) && newValue : v; });
return found ? n : arr;
}
exports.array_replace = array_replace;
/**
* Push values onto an array.
*
* @param arr the array
* @param values the values to push
* @returns a new (frozen) instance of the array with the values pushed
*/
function array_push(arr) {
var values = [];
for (var _i = 1; _i < arguments.length; _i++) {
values[_i - 1] = arguments[_i];
}
if (values.length === 0) {
return arr;
}
var newArr = arr.slice();
newArr.push.apply(newArr, values);
FREEZING_ENABLED && deepFreeze(values);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_push = array_push;
/**
* Shift a value off the array.
*
* If the array is empty the existing array is returned.
*
* @param arr the array
* @returns a new (frozen) instance of the array with an entry shifted
*/
function array_shift(arr) {
if (arr.length === 0) {
return arr;
}
var newArr = arr.slice();
newArr.shift();
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_shift = array_shift;
/**
* Pops a value off the array.
*
* If the array is empty the existing array is returned.
*
* @param arr the array
* @returns a new (frozen) instance of the array with an entry popped
*/
function array_pop(arr) {
if (arr.length === 0) {
return arr;
}
var newArr = arr.slice(0, -1);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_pop = array_pop;
/**
* Unshift values onto the array.
*
* @param arr the array
* @returns a new (frozen) instance of the array with the `values` `unshift`ed
*/
function array_unshift(arr) {
var values = [];
for (var _i = 1; _i < arguments.length; _i++) {
values[_i - 1] = arguments[_i];
}
if (values.length === 0) {
return arr;
}
var newArr = arr.slice();
newArr.unshift.apply(newArr, values);
FREEZING_ENABLED && deepFreeze(values);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_unshift = array_unshift;
/**
* Slice off a portion of the array.
*
* @param arr the array
* @param start the start of the slice
* @param end the end of the slice (default: the end)
* @returns a slice of the array
*/
function array_slice(arr, start, end) {
if (start === 0 && (end === undefined || arr.length <= end)) {
return arr;
}
var newArr = arr.slice(start, end);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_slice = array_slice;
/**
* Insert values into an array.
*
* @param arr the array
* @param index the index to insert at
* @param values the values to insert
* @returns a new (frozen) instance of the array with the `values` inserted at `index`
*/
function array_insert(arr, index) {
var values = [];
for (var _i = 2; _i < arguments.length; _i++) {
values[_i - 2] = arguments[_i];
}
if (values.length === 0) {
return arr;
}
var newArr = arr.slice();
newArr.splice.apply(newArr, tslib_1.__spreadArray([index, 0], values));
FREEZING_ENABLED && deepFreeze(values);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_insert = array_insert;
/**
* Map the entries of the array to (potentially) new values.
*
* @param arr the array
* @param callbackFn the mapping function
* @param context the context to execute `callbackFn`
* @returns a new mapped array
*/
function array_map(arr, callbackFn, context) {
if (arr.length === 0) {
return arr;
}
var mapped = arr.map(callbackFn, context);
FREEZING_ENABLED && deepFreeze(mapped);
return mapped;
}
exports.array_map = array_map;
/**
* Filter the entries of an array.
*
* @param arr the array
* @param filterFn the filter function
* @param context the context to execute `filterFn`
* @returns a new filtered array
*/
function array_filter(arr, filterFn, context) {
if (arr.length === 0) {
return arr;
}
var filtered = arr.filter(filterFn, context);
var newArr = (filtered.length === arr.length) ? arr : filtered;
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_filter = array_filter;
function arrayEqualsThis(o, i) {
return o === this[i];
}
/**
* Sort an array
*
* @param arr the array
* @param sortFn the sort comparison function
* @returns a new sorted array
*/
function array_sort(arr, sortFn) {
if (arr.length <= 1) {
return arr;
}
var newArr = arr
.slice()
.sort(sortFn);
if (newArr.every(arrayEqualsThis, arr)) {
return arr;
}
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
exports.array_sort = array_sort;
function write(data, pathOrKey, factory) {
var path = Array.isArray(pathOrKey) ? pathOrKey : [pathOrKey];
//Follow the path into the object, except for the last value being replaced
var objs = [data];
for (var i = 0; i < path.length; i++) {
// Support non-existing parent values for the removeValue case
if (factory === REMOVE_VALUE_FN && !objs[i].hasOwnProperty(path[i])) {
return data;
}
objs.push(objs[i][path[i]]);
}
//Replace the last object with the new value
objs[objs.length - 1] = factory(objs[objs.length - 1], data);
//Write the new immutable data back into the objects
for (var i = objs.length - 2; i >= 0; i--) {
var key = path[i];
var obj = objs[i];
var val = objs[i + 1];
if (Array.isArray(obj)) {
objs[i] = array_set(obj, key, val);
}
else {
objs[i] = object_set(obj, key, val);
}
}
return objs[0];
}
exports.write = write;
function read(data, path) {
var obj = data;
for (var _i = 0, path_1 = path; _i < path_1.length; _i++) {
var p = path_1[_i];
obj = obj[p];
}
return obj;
}
function writeValue(data, pathOrKey, value) {
return write(data, pathOrKey, valueFn(value));
}
exports.writeValue = writeValue;
function writeValues(data, pathOrKey, values) {
var path = Array.isArray(pathOrKey) ? pathOrKey : [pathOrKey];
var oldValue = read(data, path);
var newValue = object_assign(oldValue, values);
return writeValue(data, path, newValue);
}
exports.writeValues = writeValues;
function removeValue(data, pathOrKey) {
return write(data, pathOrKey, REMOVE_VALUE_FN);
}
exports.removeValue = removeValue;
/**
* `delete` multiple properties from an object.
*
* If none of the values exist the existing object is returned.
*
* @param data the object
* @param keys the properties to `delete`
* @returns a new (frozen) instance of the object with all `keys` deleted
*/
function removeValues(data) {
var keys = [];
for (var _i = 1; _i < arguments.length; _i++) {
keys[_i - 1] = arguments[_i];
}
var newValue;
for (var _a = 0, keys_1 = keys; _a < keys_1.length; _a++) {
var key = keys_1[_a];
if (data.hasOwnProperty(key)) {
if (newValue === undefined) {
newValue = shallowCloneObject(data);
}
delete newValue[key];
}
}
FREEZING_ENABLED && shallowFreeze(newValue);
return newValue ? newValue : data;
}
exports.removeValues = removeValues;
//# sourceMappingURL=imuter.js.map