UNPKG

baobab

Version:

JavaScript persistent data tree with cursors.

597 lines (494 loc) 17.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.arrayFrom = arrayFrom; exports.before = before; exports.coercePath = coercePath; exports.getIn = getIn; exports.makeError = makeError; exports.hashPath = hashPath; exports.solveRelativePath = solveRelativePath; exports.solveUpdate = solveUpdate; exports.splice = splice; exports.uniqid = exports.deepMerge = exports.shallowMerge = exports.deepFreeze = exports.freeze = exports.deepClone = exports.shallowClone = exports.Archive = void 0; var _monkey = require("./monkey"); var _type = _interopRequireDefault(require("./type")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var hasOwnProp = {}.hasOwnProperty; /** * Function returning the index of the first element of a list matching the * given predicate. * * @param {array} a - The target array. * @param {function} fn - The predicate function. * @return {mixed} - The index of the first matching item or -1. */ function index(a, fn) { var i, l; for (i = 0, l = a.length; i < l; i++) { if (fn(a[i])) return i; } return -1; } /** * Efficient slice function used to clone arrays or parts of them. * * @param {array} array - The array to slice. * @return {array} - The sliced array. */ function slice(array) { var newArray = new Array(array.length); var i, l; for (i = 0, l = array.length; i < l; i++) { newArray[i] = array[i]; } return newArray; } /** * Archive abstraction * * @constructor * @param {integer} size - Maximum number of records to store. */ var Archive = /*#__PURE__*/ function () { function Archive(size) { _classCallCheck(this, Archive); this.size = size; this.records = []; } /** * Method retrieving the records. * * @return {array} - The records. */ _createClass(Archive, [{ key: "get", value: function get() { return this.records; } /** * Method adding a record to the archive * * @param {object} record - The record to store. * @return {Archive} - The archive itself for chaining purposes. */ }, { key: "add", value: function add(record) { this.records.unshift(record); // If the number of records is exceeded, we truncate the records if (this.records.length > this.size) this.records.length = this.size; return this; } /** * Method clearing the records. * * @return {Archive} - The archive itself for chaining purposes. */ }, { key: "clear", value: function clear() { this.records = []; return this; } /** * Method to go back in time. * * @param {integer} steps - Number of steps we should go back by. * @return {number} - The last record. */ }, { key: "back", value: function back(steps) { var record = this.records[steps - 1]; if (record) this.records = this.records.slice(steps); return record; } }]); return Archive; }(); /** * Function creating a real array from what should be an array but is not. * I'm looking at you nasty `arguments`... * * @param {mixed} culprit - The culprit to convert. * @return {array} - The real array. */ exports.Archive = Archive; function arrayFrom(culprit) { return slice(culprit); } /** * Function decorating one function with another that will be called before the * decorated one. * * @param {function} decorator - The decorating function. * @param {function} fn - The function to decorate. * @return {function} - The decorated function. */ function before(decorator, fn) { return function () { decorator.apply(null, arguments); fn.apply(null, arguments); }; } /** * Function cloning the given regular expression. Supports `y` and `u` flags * already. * * @param {RegExp} re - The target regular expression. * @return {RegExp} - The cloned regular expression. */ function cloneRegexp(re) { var pattern = re.source; var flags = ''; if (re.global) flags += 'g'; if (re.multiline) flags += 'm'; if (re.ignoreCase) flags += 'i'; if (re.sticky) flags += 'y'; if (re.unicode) flags += 'u'; return new RegExp(pattern, flags); } /** * Function cloning the given variable. * * @todo: implement a faster way to clone an array. * * @param {boolean} deep - Should we deep clone the variable. * @param {mixed} item - The variable to clone * @return {mixed} - The cloned variable. */ function cloner(deep, item) { if (!item || _typeof(item) !== 'object' || item instanceof Error || item instanceof _monkey.MonkeyDefinition || item instanceof _monkey.Monkey || 'ArrayBuffer' in global && item instanceof ArrayBuffer) return item; // Array if (_type["default"].array(item)) { if (deep) { var a = new Array(item.length); for (var i = 0, l = item.length; i < l; i++) { a[i] = cloner(true, item[i]); } return a; } return slice(item); } // Date if (item instanceof Date) return new Date(item.getTime()); // RegExp if (item instanceof RegExp) return cloneRegexp(item); // Object if (_type["default"].object(item)) { var o = {}; // NOTE: could be possible to erase computed properties through `null`. var props = Object.getOwnPropertyNames(item); for (var _i = 0, _l = props.length; _i < _l; _i++) { var name = props[_i]; var k = Object.getOwnPropertyDescriptor(item, name); if (k.enumerable === true) { if (k.get && k.get.isLazyGetter) { Object.defineProperty(o, name, { get: k.get, enumerable: true, configurable: true }); } else { o[name] = deep ? cloner(true, item[name]) : item[name]; } } else if (k.enumerable === false) { Object.defineProperty(o, name, { value: deep ? cloner(true, k.value) : k.value, enumerable: false, writable: true, configurable: true }); } } return o; } return item; } /** * Exporting shallow and deep cloning functions. */ var shallowClone = cloner.bind(null, false), deepClone = cloner.bind(null, true); exports.deepClone = deepClone; exports.shallowClone = shallowClone; /** * Coerce the given variable into a full-fledged path. * * @param {mixed} target - The variable to coerce. * @return {array} - The array path. */ function coercePath(target) { if (target || target === 0 || target === '') return target; return []; } /** * Function comparing an object's properties to a given descriptive * object. * * @param {object} object - The object to compare. * @param {object} description - The description's mapping. * @return {boolean} - Whether the object matches the description. */ function compare(object, description) { var ok = true, k; // If we reached here via a recursive call, object may be undefined because // not all items in a collection will have the same deep nesting structure. if (!object) return false; for (k in description) { if (_type["default"].object(description[k])) { ok = ok && compare(object[k], description[k]); } else if (_type["default"].array(description[k])) { ok = ok && !!~description[k].indexOf(object[k]); } else { if (object[k] !== description[k]) return false; } } return ok; } /** * Function freezing the given variable if possible. * * @param {boolean} deep - Should we recursively freeze the given objects? * @param {object} o - The variable to freeze. * @return {object} - The merged object. */ function freezer(deep, o) { if (_typeof(o) !== 'object' || o === null || o instanceof _monkey.Monkey) return; Object.freeze(o); if (!deep) return; if (Array.isArray(o)) { // Iterating through the elements var i, l; for (i = 0, l = o.length; i < l; i++) { deepFreeze(o[i]); } } else { var p, k; for (k in o) { if (_type["default"].lazyGetter(o, k)) continue; p = o[k]; if (!p || !hasOwnProp.call(o, k) || _typeof(p) !== 'object' || Object.isFrozen(p)) continue; deepFreeze(p); } } } var freeze = freezer.bind(null, false), deepFreeze = freezer.bind(null, true); exports.deepFreeze = deepFreeze; exports.freeze = freeze; /** * Function retrieving nested data within the given object and according to * the given path. * * @todo: work if dynamic path hit objects also. * @todo: memoized perfgetters. * * @param {object} object - The object we need to get data from. * @param {array} path - The path to follow. * @return {object} result - The result. * @return {mixed} result.data - The data at path, or `undefined`. * @return {array} result.solvedPath - The solved path or `null`. * @return {boolean} result.exists - Does the path exists in the tree? */ var NOT_FOUND_OBJECT = { data: undefined, solvedPath: null, exists: false }; function getIn(object, path) { if (!path) return NOT_FOUND_OBJECT; var solvedPath = []; var exists = true, c = object, idx, i, l; for (i = 0, l = path.length; i < l; i++) { if (!c) return { data: undefined, solvedPath: solvedPath.concat(path.slice(i)), exists: false }; if (typeof path[i] === 'function') { if (!_type["default"].array(c)) return NOT_FOUND_OBJECT; idx = index(c, path[i]); if (!~idx) return NOT_FOUND_OBJECT; solvedPath.push(idx); c = c[idx]; } else if (_typeof(path[i]) === 'object') { if (!_type["default"].array(c)) return NOT_FOUND_OBJECT; idx = index(c, function (e) { return compare(e, path[i]); }); if (!~idx) return NOT_FOUND_OBJECT; solvedPath.push(idx); c = c[idx]; } else { solvedPath.push(path[i]); exists = _typeof(c) === 'object' && path[i] in c; c = c[path[i]]; } } return { data: c, solvedPath: solvedPath, exists: exists }; } /** * Little helper returning a JavaScript error carrying some data with it. * * @param {string} message - The error message. * @param {object} [data] - Optional data to assign to the error. * @return {Error} - The created error. */ function makeError(message, data) { var err = new Error(message); for (var k in data) { err[k] = data[k]; } return err; } /** * Function taking n objects to merge them together. * Note 1): the latter object will take precedence over the first one. * Note 2): the first object will be mutated to allow for perf scenarios. * Note 3): this function will consider monkeys as leaves. * * @param {boolean} deep - Whether the merge should be deep or not. * @param {...object} objects - Objects to merge. * @return {object} - The merged object. */ function merger(deep) { for (var _len = arguments.length, objects = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { objects[_key - 1] = arguments[_key]; } var o = objects[0]; var t, i, l, k; for (i = 1, l = objects.length; i < l; i++) { t = objects[i]; for (k in t) { if (deep && _type["default"].object(t[k]) && !(t[k] instanceof _monkey.Monkey) && k !== '__proto__' && k !== 'constructor' && k !== 'prototype') { o[k] = merger(true, o[k] || {}, t[k]); } else { o[k] = t[k]; } } } return o; } /** * Exporting both `shallowMerge` and `deepMerge` functions. */ var shallowMerge = merger.bind(null, false), deepMerge = merger.bind(null, true); exports.deepMerge = deepMerge; exports.shallowMerge = shallowMerge; /** * Function returning a string hash from a non-dynamic path expressed as an * array. * * @param {array} path - The path to hash. * @return {string} string - The resultant hash. */ function hashPath(path) { return 'λ' + path.map(function (step) { if (_type["default"]["function"](step) || _type["default"].object(step)) return "#".concat(uniqid(), "#"); return step; }).join('λ'); } /** * Solving a potentially relative path. * * @param {array} base - The base path from which to solve the path. * @param {array} to - The subpath to reach. * @param {array} - The solved absolute path. */ function solveRelativePath(base, to) { var solvedPath = []; // Coercing to array to = [].concat(to); for (var i = 0, l = to.length; i < l; i++) { var step = to[i]; if (step === '.') { if (!i) solvedPath = base.slice(0); } else if (step === '..') { solvedPath = (!i ? base : solvedPath).slice(0, -1); } else { solvedPath.push(step); } } return solvedPath; } /** * Function determining whether some paths in the tree were affected by some * updates that occurred at the given paths. This helper is mainly used at * cursor level to determine whether the cursor is concerned by the updates * fired at tree level. * * NOTES: 1) If performance become an issue, the following threefold loop * can be simplified to a complex twofold one. * 2) A regex version could also work but I am not confident it would * be faster. * 3) Another solution would be to keep a register of cursors like with * the monkeys and update along this tree. * * @param {array} affectedPaths - The paths that were updated. * @param {array} comparedPaths - The paths that we are actually interested in. * @return {boolean} - Is the update relevant to the compared * paths? */ function solveUpdate(affectedPaths, comparedPaths) { var i, j, k, l, m, n, p, c, s; // Looping through possible paths for (i = 0, l = affectedPaths.length; i < l; i++) { p = affectedPaths[i]; if (!p.length) return true; // Looping through logged paths for (j = 0, m = comparedPaths.length; j < m; j++) { c = comparedPaths[j]; if (!c || !c.length) return true; // Looping through steps for (k = 0, n = c.length; k < n; k++) { s = c[k]; // If path is not relevant, we break // NOTE: the '!=' instead of '!==' is required here! if (s != p[k]) break; // If we reached last item and we are relevant if (k + 1 === n || k + 1 === p.length) return true; } } } return false; } /** * Non-mutative version of the splice array method. * * @param {array} array - The array to splice. * @param {integer} startIndex - The start index. * @param {integer} nb - Number of elements to remove. * @param {...mixed} elements - Elements to append after splicing. * @return {array} - The spliced array. */ function splice(array, startIndex, nb) { for (var _len2 = arguments.length, elements = new Array(_len2 > 3 ? _len2 - 3 : 0), _key2 = 3; _key2 < _len2; _key2++) { elements[_key2 - 3] = arguments[_key2]; } if (nb === undefined && arguments.length === 2) nb = array.length - startIndex;else if (nb === null || nb === undefined) nb = 0;else if (isNaN(+nb)) throw new Error("argument nb ".concat(nb, " can not be parsed into a number!")); nb = Math.max(0, nb); // Solving startIndex if (_type["default"]["function"](startIndex)) startIndex = index(array, startIndex); if (_type["default"].object(startIndex)) startIndex = index(array, function (e) { return compare(e, startIndex); }); // Positive index if (startIndex >= 0) return array.slice(0, startIndex).concat(elements).concat(array.slice(startIndex + nb)); // Negative index return array.slice(0, array.length + startIndex).concat(elements).concat(array.slice(array.length + startIndex + nb)); } /** * Function returning a unique incremental id each time it is called. * * @return {integer} - The latest unique id. */ var uniqid = function () { var i = 0; return function () { return i++; }; }(); exports.uniqid = uniqid;