UNPKG

baobab

Version:

JavaScript persistent data tree with cursors.

302 lines (260 loc) 6.55 kB
/** * Baobab Update * ============== * * The tree's update scheme. */ import type from './type'; import { freeze, deepFreeze, deepMerge, makeError, shallowClone, shallowMerge, splice } from './helpers'; function err(operation, expectedTarget, path) { return makeError( `Baobab.update: cannot apply the "${operation}" on ` + `a non ${expectedTarget} (path: /${path.join('/')}).`, {path} ); } /** * Function aiming at applying a single update operation on the given tree's * data. * * @param {mixed} data - The tree's data. * @param {path} path - Path of the update. * @param {object} operation - The operation to apply. * @param {object} [opts] - Optional options. * @return {mixed} - Both the new tree's data and the updated node. */ export default function update(data, path, operation, opts = {}) { const {type: operationType, value, options: operationOptions = {}} = operation; // Dummy root, so we can shift and alter the root const dummy = {root: data}, dummyPath = ['root', ...path], currentPath = []; // Walking the path let p = dummy, i, l, s; for (i = 0, l = dummyPath.length; i < l; i++) { // Current item's reference is therefore p[s] // The reason why we don't create a variable here for convenience // is because we actually need to mutate the reference. s = dummyPath[i]; // Updating the path if (i > 0) currentPath.push(s); // If we reached the end of the path, we apply the operation if (i === l - 1) { /** * Set */ if (operationType === 'set') { // Purity check if (opts.pure && p[s] === value) return {node: p[s]}; if (type.lazyGetter(p, s)) { Object.defineProperty(p, s, { value, enumerable: true, configurable: true }); } else if (opts.persistent && !operationOptions.mutableLeaf) { p[s] = shallowClone(value); } else { p[s] = value; } } /** * Monkey */ else if (operationType === 'monkey') { Object.defineProperty(p, s, { get: value, enumerable: true, configurable: true }); } /** * Apply */ else if (operationType === 'apply') { const result = value(p[s]); // Purity check if (opts.pure && p[s] === result) return {node: p[s]}; if (type.lazyGetter(p, s)) { Object.defineProperty(p, s, { value: result, enumerable: true, configurable: true }); } else if (opts.persistent) { p[s] = shallowClone(result); } else { p[s] = result; } } /** * Push */ else if (operationType === 'push') { if (!type.array(p[s])) throw err( 'push', 'array', currentPath ); if (opts.persistent) p[s] = p[s].concat([value]); else p[s].push(value); } /** * Unshift */ else if (operationType === 'unshift') { if (!type.array(p[s])) throw err( 'unshift', 'array', currentPath ); if (opts.persistent) p[s] = [value].concat(p[s]); else p[s].unshift(value); } /** * Concat */ else if (operationType === 'concat') { if (!type.array(p[s])) throw err( 'concat', 'array', currentPath ); if (opts.persistent) p[s] = p[s].concat(value); else p[s].push.apply(p[s], value); } /** * Splice */ else if (operationType === 'splice') { if (!type.array(p[s])) throw err( 'splice', 'array', currentPath ); if (opts.persistent) p[s] = splice.apply(null, [p[s]].concat(value)); else p[s].splice.apply(p[s], value); } /** * Pop */ else if (operationType === 'pop') { if (!type.array(p[s])) throw err( 'pop', 'array', currentPath ); if (opts.persistent) p[s] = splice(p[s], -1, 1); else p[s].pop(); } /** * Shift */ else if (operationType === 'shift') { if (!type.array(p[s])) throw err( 'shift', 'array', currentPath ); if (opts.persistent) p[s] = splice(p[s], 0, 1); else p[s].shift(); } /** * Unset */ else if (operationType === 'unset') { if (type.object(p)) delete p[s]; else if (type.array(p)) p.splice(s, 1); } /** * Merge */ else if (operationType === 'merge') { if (!type.object(p[s])) throw err( 'merge', 'object', currentPath ); if (opts.persistent) p[s] = shallowMerge({}, p[s], value); else p[s] = shallowMerge(p[s], value); } /** * Deep merge */ else if (operationType === 'deepMerge') { if (!type.object(p[s])) throw err( 'deepMerge', 'object', currentPath ); if (opts.persistent) p[s] = deepMerge({}, p[s], value); else p[s] = deepMerge(p[s], value); } // Deep freezing the resulting value if (opts.immutable && !operationOptions.mutableLeaf) deepFreeze(p); break; } // If we reached a leaf, we override by setting an empty object else if (type.primitive(p[s])) { p[s] = {}; } // Else, we shift the reference and continue the path else if (opts.persistent) { p[s] = shallowClone(p[s]); } // Should we freeze the current step before continuing? if (opts.immutable && l > 0) freeze(p); p = p[s]; } // If we are updating a dynamic node, we need not return the affected node if (type.lazyGetter(p, s)) return {data: dummy.root}; // Returning new data object return {data: dummy.root, node: p[s]}; }