lib/lseqtree.js
'use strict';
const merge = require('lodash.merge');
const Base = require('./base.js');
const Strategy = require('./strategy.js');
const Identifier = require('./identifier.js');
const Triple = require('./triple.js');
const LSeqNode = require('./lseqnode.js');
const ExOutOfBounds = require('./exoutofbounds.js');
/**
* Distributed array using LSeq allocation strategy with an underlying
* exponential tree.
*/
class LSeqTree {
/**
* @param {Object} source The globally unique site identifier.
* @param {Object} [options] The options of the LSeqTree.
* @param {Number} [options.boundary = 10] The maximal interval between two
* generated nodes.
* @param {Number} [options.base = 15] The base, i.e., the maximal arity of
* the root node. Default is 2**15.
*/
constructor (site, options = {}) {
let listTriple;
// #0 process options
this.options = merge({ boundary: 10, base: 15 }, options);
// #1 initialize source, counter, and strategy choice
this._s = site;
this._c = 0;
this._hash = (depth) => depth%2;
this._base = new Base(this.options.base);
this._strategy = new Strategy(this._base, this.options.boundary);
// #2 initialize tree structure with maximal bounds
this.root = new LSeqNode();
// #A minimal bound
this.root.add(new LSeqNode([new Triple(0,0,0)], ''));
// #B maximal bound
this.root.add(
new LSeqNode([new Triple(Math.pow(2, this._base.getBitBase(0)) - 1,
Number.MAX_VALUE,
Number.MAX_VALUE)], ''));
};
get length () {
let result = this.root.subCounter - 2; // -2: the boundaries
result = (this.root._hasElement && result + 1) || result;
return result;
};
/**
* Get the element at targeted index in the linearized sequence. It does not
* take into account the hidden boundaries of the sequence [MIN, e_1, e_2,
* ... e_length, MAX], hence index of e_1 is 0.
* @param {Number} index The index of the element in the flattened array.
* @return {Object} The element located at the index in argument.
*/
get (index) {
if (index < 0 || index >= this.length) {
throw new ExOutOfBounds(index, this.length);
};
let node = this.root.get(index + 1);
while (!node.isLeaf){
node = node.child;
};
return node.e;
};
/**
* @private Get the LSeqNode at targeted index in the linearized
* sequence. The sequence includes the hidden boundaries [MIN, e_1, e_2,
* ... e_length, MAX], hence e_1's index is 1.
* @param {Number} index The index of the element in the flattened array.
* @return {LSeqNode} The LSeqNode targeting the element at index.
*/
_get (index) {
if (index < 0 || index >= this.length + 2) { // +2: boundaries
throw new ExOutOfBounds(index, this.length + 2);
};
return this.root.get(index);
};
/**
* Insert a value at the targeted index.
* @param {Object} element The element to insert, e.g. a character if the
* sequence is a string.
* @param {Number} index The position in the array.
* @return {Object} {_e: element of Object type, _i: Identifier}
*/
insert (element, index) {
const pei = this._get(index), // #1a previous bound
qei = this._get(index+1); // #1b next bound
// #2a incrementing the local counter
this._c += 1;
// #2b generating the id inbetween the bounds
const id = this.alloc(pei, qei);
// #3 add it to the structure and return value
const pair = {elem: element, id: id};
this.applyInsert(pair);
return pair;
};
/**
* Delete the element at the index.
* @param {Number} index The index of the element to delete in the array.
* @return {Identifier} The identifier of the element at the index.
*/
remove (index) {
const ei = this._get(index + 1);
const i = new Identifier(this._base).fromNode(ei);
this.applyRemove(ei);
return i;
};
/**
* Generate the digit part of the identifiers between p and q.
* @param {LSeqNode} p The digit part of the previous identifier.
* @param {LSeqNode} q The digit part of the next identifier.
* @return {Identifier} The new identifier located between p and q.
*/
alloc (p, q) {
let interval = 0, level = 0;
// #1 process the level of the new identifier
while (interval <= 0) { // no room for insertion
interval = this._base.getInterval(level, p, q);
++level;
};
level -= 1;
if (this._hash(level) === 0) {
return this._strategy.bPlus(p, q,
level, interval,
this._s, this._c);
} else {
return this._strategy.bMinus(p, q,
level, interval,
this._s, this._c);
};
};
/**
* Insert an element created from a remote site into the array.
* @param {Object} pair Pair containing the identifier and the element to
* insert in the data structure.
* @param {Identifier|LSeqNode} pair.id The identifier of the element.
* @param {Object} pair.elem The element to insert.
* @param {boolean} [noIndex = true] Whether or not it should return the
* index of the insert.
* @return {Number|Boolean} The index of the newly inserted element in the
* array, if asked. -1 if the element already exists and has not been added.
* If noIndex, returns true if the element has been added, false otherwise.
*/
applyInsert (pair, noIndex = true) {
let node, result, i;
// #0 cast from the proper type
// #0A the identifier is an Identifier
i = pair.id;
node = i && i._d && i._s && i._c &&
(new Identifier(this._base, i._d, i._s, i._c).toNode(pair.elem));
// #0B the identifier is a LSeqNode
node = (i && i.t && i.children && LSeqNode.fromJSON(i)) || node;
// #1 integrates the new element to the data structure
result = this.root.add(node);
// #2 if the element as been added
if (noIndex) {
return result;
} else if (result) {
return this.root.indexOf(node);
} else {
return -1;
};
};
/**
* Delete the element with the targeted identifier.
* @param {Identifier|LSeqNode} i The identifier of the element.
* @return {Number} The index of the element freshly deleted, -1 if no
* removal.
*/
applyRemove (i) {
let node, position;
// #0 cast from the proper type
node = i && i._d && i._s && i._c &&
(new Identifier(this._base, i._d, i._s, i._c)).toNode(null);
// #0B the identifier is a LSEQNode
node = (i && i.t && i.children && LSeqNode.fromJSON(i)) || node;
// #1 get the index of the element to remove
position = this.root.indexOf(node);
if (position !== -1){
// #2 if it exists remove it
this.root.del(node);
};
return position;
};
/**
* Cast the JSON object into a proper LSeqTree.
* @param {Object} object the JSON object to cast.
* @return {LSeqTree} A self reference.
*/
fromJSON (object) {
// #1 copy the source, counter, and length of the object
this._s = object._s;
this._c = object._c;
this.options = object.options;
this._base = new Base(this.options.base);
this._boundary = new Strategy(this._base, this.options.boundary);
// #2 depth first adding
const depthFirst = (currentNode, currentPath) => {
const triple = new Triple(currentNode.t.p,
currentNode.t.s,
currentNode.t.c);
currentPath.push(triple); // stack
if (currentNode.e !== null) {
this.root.add(new LSeqNode(currentPath.slice(), currentNode.e));
};
for (let i = 0; i < currentNode.children.length; ++i) {
depthFirst(currentNode.children[i], currentPath);
};
currentPath.pop(); // unstack
};
for (let i = 0; i < object.root.children.length; ++i){
depthFirst(object.root.children[i], []);
};
return this;
};
};
module.exports = LSeqTree;