baobab
Version:
JavaScript persistent data tree with cursors.
624 lines (531 loc) • 15 kB
JavaScript
/* eslint eqeqeq: 0 */
/* eslint no-use-before-define: 0 */
/**
* Baobab Helpers
* ===============
*
* Miscellaneous helper functions.
*/
import {Monkey, MonkeyDefinition} from './monkey';
import type from './type';
const 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) {
let 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) {
const newArray = new Array(array.length);
let 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.
*/
export class Archive {
constructor(size) {
this.size = size;
this.records = [];
}
/**
* Method retrieving the records.
*
* @return {array} - The records.
*/
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.
*/
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.
*/
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.
*/
back(steps) {
const record = this.records[steps - 1];
if (record)
this.records = this.records.slice(steps);
return record;
}
}
/**
* 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.
*/
export 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.
*/
export 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) {
const pattern = re.source;
let 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 MonkeyDefinition ||
item instanceof Monkey ||
('ArrayBuffer' in global && item instanceof ArrayBuffer))
return item;
// Array
if (type.array(item)) {
if (deep) {
const a = new Array(item.length);
for (let 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.object(item)) {
const o = {};
// NOTE: could be possible to erase computed properties through `null`.
const props = Object.getOwnPropertyNames(item);
for (let i = 0, l = props.length; i < l; i++) {
const name = props[i];
const 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.
*/
const shallowClone = cloner.bind(null, false),
deepClone = cloner.bind(null, true);
export {shallowClone, deepClone};
/**
* Coerce the given variable into a full-fledged path.
*
* @param {mixed} target - The variable to coerce.
* @return {array} - The array path.
*/
export 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) {
let 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.object(description[k])) {
ok = ok && compare(object[k], description[k]);
}
else if (type.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)
return;
Object.freeze(o);
if (!deep)
return;
if (Array.isArray(o)) {
// Iterating through the elements
let i,
l;
for (i = 0, l = o.length; i < l; i++)
deepFreeze(o[i]);
}
else {
let p,
k;
for (k in o) {
if (type.lazyGetter(o, k))
continue;
p = o[k];
if (!p ||
!hasOwnProp.call(o, k) ||
typeof p !== 'object' ||
Object.isFrozen(p))
continue;
deepFreeze(p);
}
}
}
const freeze = freezer.bind(null, false),
deepFreeze = freezer.bind(null, true);
export {freeze, deepFreeze};
/**
* 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?
*/
const NOT_FOUND_OBJECT = {data: undefined, solvedPath: null, exists: false};
export function getIn(object, path) {
if (!path)
return NOT_FOUND_OBJECT;
const solvedPath = [];
let 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.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.array(c))
return NOT_FOUND_OBJECT;
idx = index(c, e => 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, 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.
*/
export function makeError(message, data) {
const err = new Error(message);
for (const 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, ...objects) {
const o = objects[0];
let t,
i,
l,
k;
for (i = 1, l = objects.length; i < l; i++) {
t = objects[i];
for (k in t) {
if (deep &&
type.object(t[k]) &&
!(t[k] instanceof 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.
*/
const shallowMerge = merger.bind(null, false),
deepMerge = merger.bind(null, true);
export {shallowMerge, deepMerge};
/**
* 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.
*/
export function hashPath(path) {
return 'λ' + path.map(step => {
if (type.function(step) || type.object(step))
return `#${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.
*/
export function solveRelativePath(base, to) {
let solvedPath = [];
// Coercing to array
to = [].concat(to);
for (let i = 0, l = to.length; i < l; i++) {
const 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?
*/
export function solveUpdate(affectedPaths, comparedPaths) {
let 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.
*/
export function splice(array, startIndex, nb, ...elements) {
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 ${nb} can not be parsed into a number!`);
nb = Math.max(0, nb);
// Solving startIndex
if (type.function(startIndex))
startIndex = index(array, startIndex);
if (type.object(startIndex))
startIndex = index(array, e => 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.
*/
const uniqid = (function() {
let i = 0;
return function() {
return i++;
};
})();
export {uniqid};