UNPKG

transmutable

Version:

immutable objects that pretend to be mutable

193 lines (166 loc) 6.08 kB
'use strict'; const { MUTATION, WAS_WRITTEN, WAS_ACCESSED, ENTITY, ENTITIES } = require('./symbols'); const { get, set } = require('./get-set'); const concatOne = (arr, s) => { const newArr = new Array(arr.length + 1); for (let i = 0; i <= arr.length; i++) { if (i < arr.length) newArr[i] = arr[i]; else newArr[i] = s; } return newArr; }; const concat = concatOne; function createStage(target, rootPatch, keys = []) { return new Proxy(target, { get(target, name) { const propPatch = concat(keys, name); let value; const mutation = get(rootPatch, concat(propPatch, MUTATION)); if (mutation) { return mutation.value; } else value = target[name]; if (value && typeof value == 'object') { return createStage(value, rootPatch, propPatch); } return value; }, set(target, name, value) { const oldValue = target[name]; if (value !== oldValue) { if (Array.isArray(target)) { let patch = get(rootPatch, keys); if (!patch) { patch = {}; set(rootPatch, keys, patch); } if (!patch[MUTATION]) { const arrayDraft = target.slice(); // we need to explicitly assign the first changed item // all next changes will affect arrayDraft directly // without Proxy (`get` trap will return patch[MUTATION].value) arrayDraft[name] = value; patch[MUTATION] = {value: arrayDraft}; } return true; } set(rootPatch, concat(keys, name), {[MUTATION]:{value}}) } return true; }, ownKeys(target) { const patch = get(rootPatch, keys.concat([])) || {} return Array.from(new Set( Reflect.ownKeys(target).concat(Object.keys(patch)) )); }, getOwnPropertyDescriptor(target, name) { const patch = get(rootPatch, keys) || {}; if (patch[name] && patch[name][MUTATION]) { return { configurable: true, enumerable: true, value: patch[name][MUTATION].value } } return Reflect.getOwnPropertyDescriptor(target, name); } }); } function applyPatch (node, patch, root, rootPatch) { if (patch && patch[MUTATION]) { const mutValue = patch[MUTATION].value; if (mutValue && mutValue[ENTITY]) { const id = mutValue[ENTITY]; if (rootPatch[ENTITIES] && rootPatch[ENTITIES] && rootPatch[ENTITIES][id]) { return rootPatch[ENTITIES][id][MUTATION].value; } return root[ENTITIES][mutValue[ENTITY]]; } return patch[MUTATION].value; } let symbols; if (node && typeof node == 'object') { symbols = Object.getOwnPropertySymbols(node); } if ( patch && node && typeof node == 'object' //&& patch[WAS_ACCESSED] && Object.keys(patch).length || symbols && symbols.length ) { let copy; if (Array.isArray(node)) copy = node.slice(); else { copy = {}; for (let k in node) { copy[k] = node[k]; } if (symbols) { for (let i = 0; i < symbols.length; i++) { copy[symbols[i]] = node[symbols[i]]; } } } for (let k in patch) { const res = applyPatch(node[k], patch[k], root, rootPatch); copy[k] = res; } const patchSymbols = Object.getOwnPropertySymbols(patch); for (let i = 0; i < patchSymbols.length; i++) { const k = patchSymbols[i]; const res = applyPatch(node[k], patch[k], root, rootPatch); copy[k] = res; } return copy; } return node; } exports.applyPatch = applyPatch; const diff = require('./diff'); const copyDeep = require('./copyDeep'); const transform = (transformer, original, ...args) => { if (typeof original == 'undefined') { return transform.bind(null, transformer); } if (typeof transformer !== 'function') throw new Error(` API was changed in 0.5.0 version of Transmutable library. Now transform function takes transforming function as a FIRST argument. Original state as a SECOND one. `); let patch; let result; if (typeof Proxy == 'undefined') { const copy = copyDeep(original); result = transformer.call(copy, copy, ...args); patch = diff(original, copy); } else { patch = {}; const stage = createStage(original, patch); result = transformer.call(stage, stage, ...args); } if (typeof result != 'undefined') return result; return applyPatch(original, patch, original, patch); } exports.transform = transform; // we keep Reducer separately because Reducer is meant for use with Redux // and Transform is for general use. // now they both share the same API and implementation // but in future versions it may not be true exports.Reducer = () => { throw new Error("Transmutable: to create Redux reducer just use `transform` function with currying (look into docs)") } const over = (getter, setter, original) => { if (typeof setter == 'undefined') return over.bind(null, getter); return transform(d => { const relativeStage = get(d, getter); const result = setter.call(relativeStage, relativeStage); if (typeof result != 'undefined') { set(d, getter, result) } }, original); } exports.over = over; exports.transformAt = over;