imuter
Version:
Immutable data library
432 lines • 13 kB
JavaScript
/// <amd-module name="imuter" />
;
//Should be determined at compile time to allow tree-shaking
const FREEZING_ENABLED = !(typeof process !== "undefined" && process.env.NODE_ENV === "production");
// eslint-disable-next-line @typescript-eslint/unbound-method
const 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 (const entry of value) {
recursiveFreeze(entry);
}
}
else {
for (const key in value) {
recursiveFreeze(value[key]);
}
}
return value;
}
function identity(value) { return value; }
function valueFn(v) { return function () { return v; }; }
const shallowFreeze = FREEZING_ENABLED ? Object.freeze : identity;
const 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
*/
export const imuter = deepFreeze;
const DELETE_VALUE = deepFreeze({});
const REMOVE_VALUE = deepFreeze({});
const 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
*/
export function object_set(obj, prop, value) {
if ((value === DELETE_VALUE || value === REMOVE_VALUE) ? !(prop in obj) : obj[prop] === value) {
return obj;
}
const 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;
}
/**
* `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
*/
export function object_delete(obj, prop) {
return object_set(obj, prop, DELETE_VALUE);
}
export function object_assign(...sources) {
const newObj = Object.assign({}, ...sources);
FREEZING_ENABLED && deepFreeze(newObj);
return newObj;
}
// 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`
*/
export function array_set(arr, index, value) {
if ((value === DELETE_VALUE || value === REMOVE_VALUE) ? !(index in arr) : arr[index] === value) {
return arr;
}
const 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;
}
/**
* `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
*/
export function array_delete(arr, index) {
return array_set(arr, index, DELETE_VALUE);
}
/**
* 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`
*/
export function array_remove(arr, index, deleteCount = 1) {
if (arr.length <= index || deleteCount === 0) {
return arr;
}
const newArr = arr.slice();
newArr.splice(index, deleteCount);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
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
*/
export function array_exclude(arr, value) {
return array_filter(arr, notEqualThis, value);
}
/**
* 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`
*/
export function array_replace(arr, oldValue, newValue) {
FREEZING_ENABLED && deepFreeze(newValue);
let found = false;
const n = array_map(arr, v => v === oldValue ? (found = true) && newValue : v);
return found ? n : arr;
}
/**
* 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
*/
export function array_push(arr, ...values) {
if (values.length === 0) {
return arr;
}
const newArr = arr.slice();
newArr.push(...values);
FREEZING_ENABLED && deepFreeze(values);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
/**
* 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
*/
export function array_shift(arr) {
if (arr.length === 0) {
return arr;
}
const newArr = arr.slice();
newArr.shift();
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
/**
* 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
*/
export function array_pop(arr) {
if (arr.length === 0) {
return arr;
}
const newArr = arr.slice(0, -1);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
/**
* Unshift values onto the array.
*
* @param arr the array
* @returns a new (frozen) instance of the array with the `values` `unshift`ed
*/
export function array_unshift(arr, ...values) {
if (values.length === 0) {
return arr;
}
const newArr = arr.slice();
newArr.unshift(...values);
FREEZING_ENABLED && deepFreeze(values);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
/**
* 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
*/
export function array_slice(arr, start, end) {
if (start === 0 && (end === undefined || arr.length <= end)) {
return arr;
}
const newArr = arr.slice(start, end);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
/**
* 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`
*/
export function array_insert(arr, index, ...values) {
if (values.length === 0) {
return arr;
}
const newArr = arr.slice();
newArr.splice(index, 0, ...values);
FREEZING_ENABLED && deepFreeze(values);
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
/**
* 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
*/
export function array_map(arr, callbackFn, context) {
if (arr.length === 0) {
return arr;
}
const mapped = arr.map(callbackFn, context);
FREEZING_ENABLED && deepFreeze(mapped);
return mapped;
}
/**
* 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
*/
export function array_filter(arr, filterFn, context) {
if (arr.length === 0) {
return arr;
}
const filtered = arr.filter(filterFn, context);
const newArr = (filtered.length === arr.length) ? arr : filtered;
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
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
*/
export function array_sort(arr, sortFn) {
if (arr.length <= 1) {
return arr;
}
const newArr = arr
.slice()
.sort(sortFn);
if (newArr.every(arrayEqualsThis, arr)) {
return arr;
}
FREEZING_ENABLED && shallowFreeze(newArr);
return newArr;
}
export function write(data, pathOrKey, factory) {
const path = Array.isArray(pathOrKey) ? pathOrKey : [pathOrKey];
//Follow the path into the object, except for the last value being replaced
const objs = [data];
for (let 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 (let i = objs.length - 2; i >= 0; i--) {
const key = path[i];
const obj = objs[i];
const 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];
}
function read(data, path) {
let obj = data;
for (const p of path) {
obj = obj[p];
}
return obj;
}
export function writeValue(data, pathOrKey, value) {
return write(data, pathOrKey, valueFn(value));
}
export function writeValues(data, pathOrKey, values) {
const path = Array.isArray(pathOrKey) ? pathOrKey : [pathOrKey];
const oldValue = read(data, path);
const newValue = object_assign(oldValue, values);
return writeValue(data, path, newValue);
}
export function removeValue(data, pathOrKey) {
return write(data, pathOrKey, REMOVE_VALUE_FN);
}
/**
* `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
*/
export function removeValues(data, ...keys) {
let newValue;
for (const key of keys) {
if (data.hasOwnProperty(key)) {
if (newValue === undefined) {
newValue = shallowCloneObject(data);
}
delete newValue[key];
}
}
FREEZING_ENABLED && shallowFreeze(newValue);
return newValue ? newValue : data;
}
//# sourceMappingURL=imuter.js.map