UNPKG

webgme-engine

Version:

WebGME server and Client API without a GUI

962 lines (769 loc) 30.1 kB
/*globals define*/ /*eslint-env node, browser*/ /** * @author mmaroti / https://github.com/mmaroti */ define([ 'common/core/CoreAssert', 'common/util/key', 'common/core/tasync', 'common/util/random', 'common/regexp', 'common/core/constants', 'common/storage/constants', 'common/core/convertData', 'common/core/CoreIllegalArgumentError', 'common/util/util', ], function (ASSERT, generateKey, TASYNC, RANDOM, REGEXP, CONSTANTS, STORAGE_CONSTANTS, convertData, IllegalArgumentError, UTIL) { 'use strict'; var rootCounter = 0; function CoreTree(storage, options) { ASSERT(typeof options === 'object'); ASSERT(typeof options.globConf === 'object'); ASSERT(typeof options.logger !== 'undefined'); var gmeConfig = options.globConf, logger = options.logger.fork('core'), ID_NAME = storage.ID_NAME, roots = [], ticks = 0, mutateCount = 0, self = this; this.loadObject = TASYNC.wrap(function (hash, callback) { storage.loadObject(hash, function (err, resultObject) { if (err) { callback(err, null); return; } if (resultObject && resultObject.type === STORAGE_CONSTANTS.COMMIT_TYPE) { err = new IllegalArgumentError('Cannot load commit object [' + hash + '] as a root node.'); resultObject = null; } callback(err, resultObject); }); }); this.loadPaths = TASYNC.wrap(storage.loadPaths); this.insertObject = storage.insertObject; this.logger = logger; this.ID_NAME = ID_NAME; // ------- memory management function __detachChildren(node) { ASSERT(UTIL.isTrueObject(node.children)); ASSERT(node.age >= CONSTANTS.MAX_AGE - 1); var children = node.children; node.children = null; node.age = CONSTANTS.MAX_AGE; for (var child in children) { __detachChildren(children[child]); } } function __ageNodes(nodes) { ASSERT(UTIL.isTrueObject(nodes)); var keys = Object.keys(nodes), node, i; for (i = 0; i < keys.length; i += 1) { node = nodes[keys[i]]; ASSERT(node.age < CONSTANTS.MAX_AGE); if (++node.age >= CONSTANTS.MAX_AGE) { delete nodes[keys[i]]; __detachChildren(node); } else { __ageNodes(node.children); } } } function __ageRoots() { var root, i; if (++ticks >= CONSTANTS.MAX_TICKS) { ticks = 0; i = roots.length; while (--i >= 0) { root = roots[i]; ASSERT(root.age < CONSTANTS.MAX_AGE); if (++root.age >= CONSTANTS.MAX_AGE) { roots.splice(i, 1); __detachChildren(root); } else { __ageNodes(root.children); } } } } function __getChildNode(children, relid) { ASSERT(UTIL.isTrueObject(children)); ASSERT(typeof relid === 'string'); if (Object.hasOwn(children, relid)) { children[relid].age = 0; return children[relid]; } return null; } function __getEmptyData() { return {}; } function __getChildData(data, relid) { ASSERT(typeof relid === 'string'); if (typeof data === 'object' && data !== null) { data = data[relid]; return data === undefined ? __getEmptyData() : data; } else { return null; } } function __isMutableData(data) { return typeof data === 'object' && data !== null && data[CONSTANTS.MUTABLE_PROPERTY] === true; } function __isEmptyData(data) { if (typeof data === 'string') { return false; } else if (typeof data === 'object' && Object.keys(data).length === 0) { return true; } else { return false; } } function __areEquivalent(data1, data2) { return data1 === data2 || (typeof data1 === 'string' && data1 === __getChildData(data2, ID_NAME)) || (__isEmptyData(data1) && __isEmptyData(data2)); } function __reloadChildrenData(node) { var key, child; for (key in node.children) { child = node.children[key]; var data = __getChildData(node.data, child.relid); if (!REGEXP.DB_HASH.test(data) || data !== __getChildData(child.data, ID_NAME)) { child.data = data; __reloadChildrenData(child); } } } function __noUnderscore(relid) { ASSERT(typeof relid === 'string'); return relid.charAt(0) !== '_'; } function __saveData(data, root, path, stackedObjects) { ASSERT(__isMutableData(data)); var cleanData; var done = __getEmptyData(), keys, key, i, child, sub, hash; delete data[CONSTANTS.MUTABLE_PROPERTY]; keys = Object.keys(data); for (i = 0; i < keys.length; i++) { key = keys[i]; child = data[key]; if (__isMutableData(child)) { sub = __saveData(child, root, path + CONSTANTS.PATH_SEP + key, stackedObjects); if (JSON.stringify(sub) === JSON.stringify(__getEmptyData())) { delete data[key]; } else { done = sub; if (typeof child[ID_NAME] === 'string') { data[key] = child[ID_NAME]; } } } else { done = undefined; } } if (done !== __getEmptyData()) { hash = data[ID_NAME]; ASSERT(hash === '' || hash === undefined); if (hash === '') { data.__v = STORAGE_CONSTANTS.VERSION; //TODO: This is a temporary fix. We should modify CANON. cleanData = JSON.parse(JSON.stringify(data)); hash = '#' + generateKey(cleanData, gmeConfig); data[ID_NAME] = hash; cleanData[ID_NAME] = hash; done = cleanData; storage.insertObject(cleanData, stackedObjects); stackedObjects[hash] = { newHash: hash, newData: cleanData, oldHash: root.initial[path] && root.initial[path].hash, oldData: root.initial[path] && root.initial[path].data }; root.initial[path] = { hash: hash, data: cleanData }; //stackedObjects[hash] = data; } } return done; } function __loadRoot2(data) { var root = { parent: null, relid: null, age: 0, children: {}, data: null, initial: { '': { hash: data[storage.ID_NAME], data: data } }, rootid: ++rootCounter }; // Ensure we get the correct version of the data. root.data = convertData(data); roots.push(root); __ageRoots(); return root; } function __loadChild2(node, newdata) { var root = self.getRoot(node), path = self.getPath(node); node = self.normalize(node); // TODO: this is a hack, we should avoid loading it multiple times if (REGEXP.DB_HASH.test(node.data)) { ASSERT(node.data === newdata[ID_NAME]); root.initial[path] = { hash: node.data, data: newdata }; // Ensure we get the correct version of the data. node.data = convertData(newdata); __reloadChildrenData(node); } else { // TODO: if this bites you, use the Cache /*if(node.data !== newdata){ console.log('kecso',node); } ASSERT(node.data === newdata);*/ } return node; } function __loadDescendantByPath2(node, path, index) { if (node === null || index === path.length) { return node; } var child = self.loadChild(node, path[index]); return TASYNC.call(__loadDescendantByPath2, child, path, index + 1); } // function __printNode(node) { // var str = '{'; // str += 'age:' + node.age; // // if (typeof node.relid === 'string') { // str += ', relid: "' + node.relid + '"'; // } // // str += ', children:'; // if (node.children === null) { // str += 'null'; // } else { // str += '['; // for (var i = 0; i < node.children.length; ++i) { // if (i !== 0) { // str += ', '; // } // str += __printNode(node.children[i]); // } // str += ']'; // } // // str += '}'; // return str; // } function __test(text, cond) { if (!cond) { throw new Error(text); } } function isValidNodeThrow(node) { __test('object', typeof node === 'object' && node !== null); __test('object 2', Object.hasOwn(node, 'parent') && Object.hasOwn(node, 'relid')); __test('parent', typeof node.parent === 'object'); __test('relid', typeof node.relid === 'string' || node.relid === null); __test('parent 2', (node.parent === null) === (node.relid === null)); __test('age', node.age >= 0 && node.age <= CONSTANTS.MAX_AGE); //__test('children', node.children === null || node.children instanceof Array); __test('children 2', (node.age === CONSTANTS.MAX_AGE) === (node.children === null)); __test('data', typeof node.data === 'object' || typeof node.data === 'string' || typeof node.data === 'number'); if (node.parent !== null) { __test('age 2', node.age >= node.parent.age); __test('mutable', !__isMutableData(node.data) || __isMutableData(node.parent.data)); } } // ------- static methods this.copyIfObject = function (val) { return typeof val === 'object' && val !== null ? JSON.parse(JSON.stringify(val)) : val; }; this.getParent = function (node) { ASSERT(typeof node.parent === 'object'); return node.parent; }; this.getRelid = function (node) { ASSERT(node.relid === null || typeof node.relid === 'string'); return node.relid; }; this.getLevel = function (node) { var level = 0; while (node.parent !== null) { ++level; node = node.parent; } return level; }; this.getRoot = function (node) { while (node.parent !== null) { node = node.parent; } return node; }; this.getPath = function (node, base) { if (node === null) { return null; } var path = ''; while (node.relid !== null && node !== base) { path = CONSTANTS.PATH_SEP + node.relid + path; node = node.parent; } return path; }; this.isValidPath = function (path) { return typeof path === 'string' && (path === '' || path.charAt(0) === CONSTANTS.PATH_SEP); }; this.splitPath = function (path) { ASSERT(self.isValidPath(path)); path = path.split(CONSTANTS.PATH_SEP); path.splice(0, 1); return path; }; this.getParentPath = function (path) { path = path.split(CONSTANTS.PATH_SEP); path.splice(-1, 1); return path.join(CONSTANTS.PATH_SEP); }; this.buildPath = function (path) { ASSERT(path instanceof Array); return path.length === 0 ? '' : CONSTANTS.PATH_SEP + path.join(CONSTANTS.PATH_SEP); }; this.joinPaths = function (first, second) { ASSERT(self.isValidPath(first) && self.isValidPath(second)); return first + second; }; this.getCommonPathPrefixData = function (first, second) { ASSERT(typeof first === 'string' && typeof second === 'string'); first = self.splitPath(first); second = self.splitPath(second); var common = []; for (var i = 0; first[i] === second[i] && i < first.length; ++i) { common.push(first[i]); } return { common: self.buildPath(common), first: self.buildPath(first.slice(i)), firstLength: first.length - i, second: self.buildPath(second.slice(i)), secondLength: second.length - i }; }; this.isPathInSubTree = function (path, subTreeRoot) { return path === subTreeRoot || path.indexOf(subTreeRoot + CONSTANTS.PATH_SEP) === 0; }; this.normalize = function (node) { ASSERT(self.isValidNode(node)); // console.log('normalize start', printNode(getRoot(node))); var parent; if (node.children === null) { ASSERT(node.age === CONSTANTS.MAX_AGE); if (node.parent !== null) { parent = self.normalize(node.parent); var temp = __getChildNode(parent.children, node.relid); if (temp !== null) { // TODO: make the current node close to the returned one // console.log('normalize end1', // printNode(getRoot(temp))); return temp; } ASSERT(node.parent.children === null || __getChildNode(node.parent.children, node.relid) === null); ASSERT(__getChildNode(parent.children, node.relid) === null); node.parent = parent; parent.children[node.relid] = node; temp = __getChildData(parent.data, node.relid); if (!REGEXP.DB_HASH.test(temp) || temp !== __getChildData(node.data, ID_NAME)) { node.data = temp; } } else { roots.push(node); } node.age = 0; node.children = {}; } else if (node.age !== 0) { parent = node; do { parent.age = 0; parent = parent.parent; } while (parent !== null && parent.age !== 0); } // console.log('normalize end2', printNode(getRoot(node))); return node; }; // ------- hierarchy this.getAncestor = function (first, second) { ASSERT(self.getRoot(first) === self.getRoot(second)); first = self.normalize(first); second = self.normalize(second); var a = []; do { a.push(first); first = first.parent; } while (first !== null); var b = []; do { b.push(second); second = second.parent; } while (second !== null); var i = a.length - 1; var j = b.length - 1; while (i !== 0 && j !== 0 && a[i - 1] === b[j - 1]) { --i; --j; } ASSERT(a[i] === b[j]); return a[i]; }; this.isAncestor = function (node, ancestor) { ASSERT(self.getRoot(node) === self.getRoot(ancestor)); node = self.normalize(node); ancestor = self.normalize(ancestor); do { if (node === ancestor) { return true; } node = node.parent; } while (node !== null); return false; }; this.createRoot = function () { var root = { parent: null, relid: null, age: 0, children: {}, data: { _mutable: true }, initial: { '': null }, rootid: ++rootCounter }; root.data[ID_NAME] = ''; roots.push(root); __ageRoots(); return root; }; this.getChild = function (node, relid) { ASSERT(typeof relid === 'string' && relid !== ID_NAME); node = self.normalize(node); var child = __getChildNode(node.children, relid); if (child !== null) { return child; } child = { parent: node, relid: relid, age: 0, children: {}, data: __getChildData(node.data, relid) }; node.children[relid] = child; __ageRoots(); return child; }; this.childLoaded = function (node, relid) { ASSERT(typeof relid === 'string' && relid !== ID_NAME); node = self.normalize(node); return __getChildNode(node.children, relid) !== null; }; this.createChild = function (node, takenRelids, minimumLength) { node = self.normalize(node); if (typeof node.data !== 'object' || node.data === null) { throw new Error('invalid node data'); } return self.getChild(node, RANDOM.generateRelid(takenRelids || node.data, minimumLength)); }; // ------- data manipulation this.isMutable = function (node) { node = self.normalize(node); return __isMutableData(node.data); }; this.isEmpty = function (node) { node = self.normalize(node); if (typeof node.data !== 'object' || node.data === null) { return false; } else if (node.data === __getEmptyData()) { return true; } return __isEmptyData(node.data); }; this.mutate = function (node) { ASSERT(self.isValidNode(node)); node = self.normalize(node); var data = node.data; if (typeof data !== 'object' || data === null) { return false; } else if (data[CONSTANTS.MUTABLE_PROPERTY] === true) { return true; } // TODO: infinite cycle if MAX_MUTATE is smaller than depth! // gmeConfig.storage.autoPersist is removed and always false var autoPersist = false; if (autoPersist && ++mutateCount > CONSTANTS.MAX_MUTATE) { mutateCount = 0; for (var i = 0; i < roots.length; ++i) { if (__isMutableData(roots[i].data)) { __saveData(roots[i].data, roots[i], ''); } } } if (node.parent !== null && !self.mutate(node.parent)) { // this should never happen return false; } var copy = __getEmptyData(); for (var key in data) { copy[key] = data[key]; } copy[CONSTANTS.MUTABLE_PROPERTY] = true; if (typeof data[ID_NAME] === 'string') { copy[ID_NAME] = ''; } if (node.parent !== null) { //inherited child doesn't have an entry in the parent as long as it has not been modified ASSERT(node.parent.data[node.relid] === undefined || __areEquivalent(__getChildData(node.parent.data, node.relid), node.data)); node.parent.data[node.relid] = copy; } node.data = copy; return true; }; this.getData = function (node) { node = self.normalize(node); ASSERT(!__isMutableData(node.data)); return node.data; }; this.setData = function (node, data) { ASSERT(data !== null && typeof data !== 'undefined'); node = self.normalize(node); if (node.parent !== null) { if (!self.mutate(node.parent)) { throw new Error('incorrect node data'); } node.parent.data[node.relid] = data; } node.data = data; __reloadChildrenData(node); }; this.deleteData = function (node) { node = self.normalize(node); if (node.parent !== null) { if (!self.mutate(node.parent)) { throw new Error('incorrect node data'); } delete node.parent.data[node.relid]; } var data = node.data; node.data = __getEmptyData(); __reloadChildrenData(node); return data; }; this.copyData = function (node) { node = self.normalize(node); if (typeof node.data !== 'object' || node.data === null) { return node.data; } // TODO: return immutable data without coping return JSON.parse(JSON.stringify(node.data)); }; this.getProperty = function (node, name) { ASSERT(typeof name === 'string' && name !== ID_NAME); var data; node = self.normalize(node); if (typeof node.data === 'object' && node.data !== null) { data = node.data[name]; } // TODO: corerel uses getProperty to get the overlay content which can get mutable // ASSERT(!__isMutableData(data)); return data; }; this.setProperty = function (node, name, data) { ASSERT(typeof name === 'string' && name !== ID_NAME); ASSERT(!__isMutableData(data) /*&& data !== null*/ && data !== undefined); //TODO is the 'null' really can be a value of a property??? node = self.normalize(node); if (!self.mutate(node)) { throw new Error('incorrect node data'); } node.data[name] = data; var child = __getChildNode(node.children, name); if (child !== null) { child.data = data; __reloadChildrenData(child); } }; this.deleteProperty = function (node, name) { ASSERT(typeof name === 'string' && name !== ID_NAME); node = self.normalize(node); if (!self.mutate(node)) { throw new Error('incorrect node data'); } delete node.data[name]; var child = __getChildNode(node.children, name); if (child !== null) { child.data = __getEmptyData(); __reloadChildrenData(child); } }; this.renameProperty = function (node, oldName, newName) { self.setProperty(node, newName, self.getProperty(node, oldName)); self.deleteProperty(node, oldName); }; this.getKeys = function (node, predicate) { var result; node = self.normalize(node); if (typeof node.data !== 'object' || node.data === null) { return null; } result = self.getRawKeys(node.data, predicate); return result; }; this.getRawKeys = function (object, predicate) { predicate = predicate || __noUnderscore; var keys = Object.keys(object); var i = keys.length; while (--i >= 0 && !predicate(keys[i])) { keys.pop(); } while (--i >= 0) { if (!predicate(keys[i])) { keys[i] = keys.pop(); } } return keys; }; // ------- persistence this.getHash = function (node) { if (node === null) { return null; } var hash; node = self.normalize(node); if (typeof node.data === 'object' && node.data !== null) { hash = node.data[ID_NAME]; } ASSERT(typeof hash === 'string' || hash === undefined); return hash; }; this.isHashed = function (node) { node = self.normalize(node); return typeof node.data === 'object' && node.data !== null && typeof node.data[ID_NAME] === 'string'; }; this.setHashed = function (node, hashed, noMutate) { ASSERT(typeof hashed === 'boolean'); node = self.normalize(node); if (!noMutate) { if (!self.mutate(node)) { throw new Error('incorrect node data'); } } if (hashed) { node.data[ID_NAME] = ''; } else { delete node.data[ID_NAME]; } ASSERT(node.children[ID_NAME] === undefined); }; this.persist = function (node, stackedObjects) { var updated = false, result; stackedObjects = stackedObjects || {}; node = self.normalize(node); //currently there is no reason to call the persist on a non-root object node = self.getRoot(node); if (!__isMutableData(node.data)) { return {rootHash: node.data[ID_NAME], objects: {}}; } updated = __saveData(node.data, node, '', stackedObjects); if (updated !== __getEmptyData()) { result = {}; result.objects = stackedObjects; result.rootHash = node.data[ID_NAME]; } else { result = {rootHash: node.data[ID_NAME], objects: {}}; } return result; }; this.loadRoot = function (hash) { ASSERT(REGEXP.DB_HASH.test(hash)); return TASYNC.call(__loadRoot2, self.loadObject(hash)); }; this.loadChild = function (node, relid) { ASSERT(self.isValidNode(node)); node = self.getChild(node, relid); if (typeof node.data === 'object') { return node.data !== null ? node : null; } else if (REGEXP.DB_HASH.test(node.data)) { // TODO: this is a hack, we should avoid loading it multiple // times return TASYNC.call(__loadChild2, node, self.loadObject(node.data)); } else { return null; } }; this.getChildHash = function (node, relid) { ASSERT(self.isValidNode(node)); node = self.getChild(node, relid); if (typeof node.data === 'object') { return node.data !== null ? self.getHash(node) : null; } else if (REGEXP.DB_HASH.test(node.data)) { // TODO: this is a hack, we should avoid loading it multiple // times return node.data; } else { return null; } }; this.loadByPath = function (node, path) { ASSERT(self.isValidNode(node)); ASSERT(path === '' || path.charAt(0) === CONSTANTS.PATH_SEP); path = path.split(CONSTANTS.PATH_SEP); return __loadDescendantByPath2(node, path, 1); }; // ------- valid ------- this.isValidNode = function (node) { try { isValidNodeThrow(node); return true; } catch (error) { logger.error(error.message, {stack: error.stack, node: node}); return false; } }; this.removeChildFromCache = function (node, relid) { delete node.children[relid]; return node; }; } return CoreTree; });