UNPKG

imh

Version:

The extremely fast immutable helper

499 lines (436 loc) 12.4 kB
let globalClonedObjects; const defaultOrderBy = x => x; export default function imh() { // create mutator if (arguments.length < 2) { return wrapMutator(arguments[0], function (model, apply) { return apply(model); }); } return imh(arguments[1])(arguments[0]); } Object.assign(imh, { set, prop, map, unset, push, merge, splice, filter, sort, orderBy, swap, remove, add, mul, div, toggle, val, clear, def, pop, shift, unshift, reverse, result, lower, upper, replace }); function wrapMutator(mutations, mutator) { if (!Array.isArray(mutations)) { mutations = [mutations]; } return function (model, data, context) { if (!context) { context = createContext(model); } return mutator(model, function (value, data, customMutations = mutations) { return applyMutations(value, context, data, customMutations); }, context, data); }; } export function push(...items) { return function pushMutation(model) { mustBeArray(model); if (!items.length) return model; return model.concat(items); }; } export function map(mutations) { return wrapMutator(mutations, function (model, apply) { mustBeArray(model); let hasChange = false; const nextModel = model.map(function (currentValue, index, array) { const data = { index, array }; let nextValue = tryMutate(apply(currentValue, data), customMutations => apply(currentValue, data, customMutations)); if (nextValue !== currentValue) { hasChange = true; return nextValue; } return currentValue; }); return hasChange ? nextModel : model; }); } export function merge(...objects) { return function (model, _, context) { let cloned = model; const changedKeys = new Set(); // use for loop for better performance for (let i = 0; i < objects.length; i++) { const object = objects[i]; const keys = Object.keys(object); for (let j = 0; j < keys.length; j++) { const key = keys[j]; const value = object[key]; if (cloned[key] !== value) { if (cloned === model) { cloned = context.clone(model); } cloned[key] = value; changedKeys.add(key); } } } if (changedKeys.size) { let hasChange = false; changedKeys.forEach(function (key) { if (model[key] !== cloned[key]) { hasChange = true; } }); if (!hasChange) return model; } return cloned; }; } export function prop(key, mutations) { if (Array.isArray(key)) { return key.reduceRight(function (prevMutations, key) { return [mutateSingleProp(key, prevMutations)]; }, mutations)[0]; } return mutateSingleProp(key, mutations); } function mutateSingleProp(key, mutations) { return wrapMutator(tryMutate(mutations, x => x, x => val(x)), function (model, apply, context) { if (typeof model === "undefined" || model === null) { model = {}; } if (typeof key === "function") { if (!Array.isArray(model)) { throw new Error("Can use dynamic key with array type model only"); } let cloned = model; for (let i = 0; i < cloned.length; i++) { const currentValue = cloned[i]; const found = key(cloned[i], i); if (found) { const nextValue = apply(currentValue); if (nextValue !== cloned[i]) { if (cloned === model) { cloned = context.clone(model); } cloned[i] = nextValue; } } } return cloned; } const currentValue = model[key]; const nextValue = apply(currentValue); if (currentValue === nextValue) { return model; } const cloned = context.clone(model); cloned[key] = nextValue; return cloned; }); } export function set(key, value) { return function (model, data, context) { if (typeof model === "undefined" || model === null) { model = {}; } if (model[key] === value) return model; const cloned = context.clone(model); cloned[key] = value; return cloned; }; } export function unset() { const args = arguments; return function (model, data, context) { let cloned = model; for (let i = 0; i < args.length; i++) { const key = args[i]; if (key in cloned) { if (cloned === model) { cloned = context.clone(model); } delete cloned[key]; } } return cloned; }; } function applyMutations(model, context, data, mutations) { return mutations.reduce(function (value, mutation) { const result = mutation(value, data, context); if (typeof result === "function") return result(value, context); return result; }, model); } function createContext(root) { const clonedObjects = globalClonedObjects = new WeakMap(); return { root, clone(obj, array) { if (typeof obj !== "object") { throw new Error("Cannot clone non-object value"); } // is cloned if (clonedObjects.has(obj)) return obj; let cloned; if (Array.isArray(obj)) { if (array === false) throw new Error("Expected object type but got array type"); cloned = obj.slice(); } else { if (array === true) throw new Error("Expected array type but got object type"); cloned = Object.assign({}, obj); } clonedObjects.set(cloned, obj); return cloned; } }; } export function splice(index, length) { const args = arguments; return function spliceMutation(model, data, context) { mustBeArray(model); const hasChange = args.length > 2 || index < model.length - 1 && length; if (!hasChange) return model; const cloned = context.clone(model); context.result = cloned.splice.apply(cloned, args); return shallowMemoArray(model, cloned); }; } export function filter(predicate) { return function filterMutation(model) { mustBeArray(model); const filtered = model.filter(predicate); return shallowMemoArray(model, filtered); }; } export function sort(compareFn) { return function sortMutation(model, data, context) { mustBeArray(model); const cloned = context.clone(model); cloned.sort(compareFn); return shallowMemoArray(model, cloned); }; } export function orderBy(selector = defaultOrderBy, direction = 1) { return sort((a, b) => { const av = selector(a); const bv = selector(b); return (av > bv ? 1 : av < bv ? -1 : 0) * direction; }); } export function swap(from, to) { return function swapMutation(model, data, context) { mustBeArray(model); const fv = model[from]; const tv = model[to]; if (fv === tv) return model; const cloned = context.clone(model); cloned[from] = tv; cloned[to] = fv; return cloned; }; } export function remove(...indices) { return function removeMutation(model, data, context) { mustBeArray(model); // remove invalid indices before to proceed const validIndices = indices.filter(index => index >= 0 && index < model.length).sort(); if (!validIndices.length) return model; const cloned = context.clone(model); while (validIndices.length) { cloned.splice(validIndices.pop(), 1); } return cloned; }; } export function add(value) { return function addMutation(model) { if (typeof model === "string") { return model + value; } // is timespan if (typeof value === "object") { const { years = 0, months = 0, days = 0, hours = 0, minutes = 0, seconds = 0, milliseconds = 0 } = value; const isTimestamp = typeof model === "number"; const from = isTimestamp ? new Date() : model; const to = new Date(from.getFullYear() + years, from.getMonth() + months, from.getDate() + days, from.getHours() + hours, from.getMinutes() + minutes, from.getSeconds() + seconds, from.getMilliseconds() + milliseconds); if (isTimestamp) { return model + to.getTime() - from.getTime(); } else { if (from.getTime() === to.getTime()) return model; return to; } } else if (model instanceof Date) { const to = new Date(model.getTime() + parseFloat(value)); if (to.getTime() === model.getTime()) return model; return to; } return model + value; }; } export function mul(value) { return function (model) { return model * value; }; } export function div(value) { return function (model) { return model / value; }; } export function replace(findWhat, replaceWith) { return function replaceMutation(model) { mustBeString(model); return model.replace(findWhat, replaceWith); }; } export function upper() { return function upperMutation(model) { mustBeString(model); return model.toUpperCase(); }; } export function lower() { return function lowerMutation(model) { mustBeString(model); return model.toLowerCase(); }; } export function toggle() { return function toggleMutation(model) { return !model; }; } export function val(value) { return function valMutation() { return value; }; } export function def(value) { return function defMutation(model = value) { return model; }; } export function clear() { return function clearMutation(model) { mustBeArray(model); return model.length ? [] : model; }; } export function pop() { return function popMutation(model, data, context) { mustBeArray(model); if (!model.length) return model; const cloned = context.clone(model); context.result = cloned.pop(); return cloned; }; } export function result(fn) { return function resultMutation(model, data, context) { return tryMutate(fn(context.result, model), mutations => applyMutations(model, context, data, mutations), () => model); }; } export function shift() { return function shiftMutation(model, data, context) { mustBeArray(model); if (!model.length) return model; const cloned = context.clone(model); context.result = cloned.shift(); return cloned; }; } export function unshift(...items) { return function unshiftMutation(model) { mustBeArray(model); if (!model.length) return items; return items.concat(model); }; } export function reverse() { return function (model, data, context) { mustBeArray(model); if (model.length < 2) return model; const cloned = context.clone(model); cloned.reverse(); return shallowMemoArray(model, cloned); }; } function mustBeArray(value) { if (!Array.isArray(value)) throw new Error("Input must be array type"); } function mustBeString(value) { if (typeof value !== "string") throw new Error("Input must be string type"); } function getOriginalValue(obj) { if (typeof obj === "object" && globalClonedObjects) { const original = globalClonedObjects.get(obj); return original || obj; } return obj; } function shallowMemo(oldValue, newValue) { oldValue = getOriginalValue(oldValue); if (typeof newValue === "function") newValue = newValue(); if (isEqual(oldValue, newValue)) return oldValue; if (Array.isArray(oldValue) && Array.isArray(newValue)) return shallowMemoArray(oldValue, newValue); return newValue; } function shallowMemoArray(oldValue, newValue) { oldValue = getOriginalValue(oldValue); if (oldValue.length === newValue.length && oldValue.every((value, index) => newValue[index] === value)) return oldValue; return newValue; } function isEqual(a, b) { if (a === b) { return true; } if (typeof a !== "object" || typeof b !== "object" || isPromiseLike(a) || isPromiseLike(b) || Array.isArray(a) || Array.isArray(b)) return false; if (a === null && b) return false; if (b === null && a) return false; const comparer = key => { return a[key] === b[key]; }; return Object.keys(a).every(comparer) && Object.keys(b).every(comparer); } function isPromiseLike(obj) { return obj && typeof obj.then === "function"; } function tryMutate(value, fn, fallback) { if (typeof value === "function") { return fn([value]); } if (Array.isArray(value) && typeof value[0] === "function") { return fn(value); } return fallback ? fallback(value) : value; } //# sourceMappingURL=index.js.map