UNPKG

total.js

Version:

MVC framework for Node.js

1,859 lines (1,533 loc) 66 kB
// Copyright 2012-2018 (c) Peter Širka <petersirka@gmail.com> // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. /** * @module FrameworkGraphDB * @version 1.0.0 */ const Fs = require('fs'); const Zlib = require('zlib'); const ZLIBOPTIONS = { level: Zlib.constants.Z_FULL_FLUSH, memLevel: Zlib.constants.Z_BEST_COMPRESSION, strategy: Zlib.constants.Z_DEFAULT_STRATEGY }; const VERSION = 1; const DOCUMENTSIZE = 1000; const PAGESIZE = 20; const PAGELIMIT = 50; const DATAOFFSET = 17; const EMPTYBUFFER = U.createBufferSize(1); const HEADERSIZE = 7000; const DELAY = 100; const REGTUNESCAPE = /%7C|%0D|%0A/g; const REGTESCAPETEST = /\||\n|\r/; const REGTESCAPE = /\||\n|\r/g; const BOOLEAN = { '1': 1, 'true': 1, 'on': 1 }; const DatabaseBuilder = framework_nosql.DatabaseBuilder; // STATES const STATE_UNCOMPRESSED = 1; const STATE_COMPRESSED = 2; const STATE_REMOVED = 255; // META const META_PAGE_ADD = 100; const META_CLASSESRELATIONS = 101; const META_PAGE_ADD3 = 102; const META_RELATIONPAGEINDEX = 103; // OPERATIONS const NEXT_READY = 1; const NEXT_INSERT = 2; const NEXT_RELATION = 3; const NEXT_UPDATE = 4; const NEXT_FIND = 5; const NEXT_REMOVE = 6; const NEXT_RESIZE = 7; const NEXT_CONTINUE = 100; // TYPES const TYPE_CLASS = 1; const TYPE_RELATION = 2; const TYPE_RELATION_DOCUMENT = 3; var IMPORTATOPERATIONS = 0; function GraphDB(name) { F.path.verify('databases'); var self = this; self.name = name; self.filename = F.path.databases(name + '.gdb'); self.filenameBackup = self.filename.replace(/\.gdb$/, '.gdp-backup'); self.ready = false; self.$classes = {}; self.$relations = {}; self.$events = {}; self.header = {}; self.pending = {}; self.pending.insert = []; self.pending.find = []; self.pending.update = []; self.pending.remove = []; self.pending.relation = []; self.pending.meta = []; self.states = {}; self.states.resize = false; self.states.insert = false; self.states.read = false; self.states.remove = false; self.states.update = false; F.path.verify('databases'); // t.open(); self.cb_error = function(err) { err && console.log(err); }; self.cb_next = function(value) { self.next(value); }; F.grapdbinstance = true; self.open(); } var GP = GraphDB.prototype; // ==== DB:HEADER (7000b) // name (30b) = from: 0 // version (1b) = from: 30 // pages (4b) = from: 31 // pagesize (2b) = from: 35 // pagelimit (2b) = from: 37 // documents (4b) = from: 39 // documentsize (2b) = from: 43 // classindex (1b) = from: 45 // relationindex (1b) = from: 46 // relationnodeindex = from: 47 // classes + relations = from: 51 // ==== DB:PAGE (20b) // type (1b) = from: 0 // index (1b) = from: 1 // documents (2b) = from: 2 // freeslots (1b) = from: 4 // parentindex (4b) = from: 5 // ==== DB:DOCUMENT (SIZE) // type (1b) = from: 0 // index (1b) = from: 1 // state (1b) = from: 2 // pageindex (4b) = from: 3 // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) // parentindex (4b) = from: 11 // size/count (2b) = from: 15 // data = from: 17 // Creates new page function addPage(self, type, index, parentindex, callback) { // @type // 1: classes // 2: relations // 3: relations private // @index // index of value // Add a new page self.header.pages++; var indexer = self.header.pages; var buffer = []; var page = U.createBufferSize(self.header.pagesize); // console.log('CREATING PAGE:', TYPES[type], indexer, type, index); page.writeUInt8(type, 0); // type (1:class, 2:relation, 3:private) page.writeUInt8(index, 1); // index page.writeUInt16LE(0, 2); // documents page.writeUInt8(0, 4); // freeslots page.writeUInt32LE(parentindex, 5); // parentindex buffer.push(page); for (var i = 0; i < self.header.pagelimit; i++) { var doc = U.createBufferSize(self.header.documentsize); doc.writeUInt8(type, 0); doc.writeUInt8(index, 1); doc.writeUInt8(STATE_REMOVED, 2); doc.writeUInt32LE(self.header.pages, 3); doc.writeUInt32LE(0, 7); // continuerindex doc.writeUInt32LE(0, 11); // parentindex doc.writeUInt16LE(0, 15); // size/count buffer.push(doc); } buffer = Buffer.concat(buffer); var offset = offsetPage(self, indexer); Fs.write(self.fd, buffer, 0, buffer.length, offset, function(err) { err && self.error(err, 'createPage.write'); !err && updMeta(self, type === TYPE_RELATION_DOCUMENT ? META_PAGE_ADD3 : META_PAGE_ADD); callback && callback(err, indexer); }); return indexer; } function addNodeFree(self, meta, callback) { if (!meta.type.findfreeslots) { addNode(self, meta, callback); return; } findDocumentFree(self, meta.type.pageindex, function(err, documentindex, pageindex) { if (!documentindex) { meta.type.findfreeslots = false; addNode(self, meta, callback); return; } var buffer = U.createBufferSize(self.header.documentsize); buffer.writeUInt8(meta.typeid, 0); // type buffer.writeUInt8(meta.type.index, 1); // index buffer.writeUInt32LE(pageindex, 3); // pageindex buffer.writeUInt8(meta.state || STATE_UNCOMPRESSED, 2); // state buffer.writeUInt32LE(meta.relationindex || 0, 7); // relationindex buffer.writeUInt32LE(meta.parentindex || 0, 11); // parentindex buffer.writeUInt16LE(meta.size, 15); meta.data && meta.data.copy(buffer, DATAOFFSET); Fs.write(self.fd, buffer, 0, buffer.length, offsetDocument(self, documentindex), function() { meta.type.locked = false; callback(null, documentindex, pageindex); }); }); } function addNode(self, meta, callback) { // meta.typeid (1 CLASS, 2 RELATION) // meta.type (link to type class/relation) // meta.state // meta.parentindex // meta.relationindex // meta.size // meta.buffer var buf = U.createBufferSize(self.header.pagesize); var offset = offsetPage(self, meta.type.pageindex); meta.type.locked = true; Fs.read(self.fd, buf, 0, buf.length, offset, function(err) { if (err) throw err; if (buf[0] !== meta.typeid) throw new Error('Not a class page'); if (!meta.type.private && buf[1] !== meta.type.index) throw new Error('Not same class type'); // type : buf[0] // index : buf[1] // documents : buf.readUInt16LE(2) // freeslots : buf[4] // parentindex : readUInt32LE(5) var buffer = U.createBufferSize(self.header.documentsize); buffer.writeUInt8(buf[0], 0); // type buffer.writeUInt8(meta.type.index, 1); // index buffer.writeUInt32LE(meta.type.pageindex, 3); // pageindex buffer.writeUInt8(meta.state || STATE_UNCOMPRESSED, 2); // state buffer.writeUInt32LE(meta.relationindex || 0, 7); // relationindex buffer.writeUInt32LE(meta.parentindex || 0, 11); // parentindex buffer.writeUInt16LE(meta.size, 15); meta.data && meta.data.copy(buffer, DATAOFFSET); var documents = buf.readUInt16LE(2); var documentsbuf = U.createBufferSize(2); documents++; documentsbuf.writeUInt16LE(documents); Fs.write(self.fd, documentsbuf, 0, documentsbuf.length, offset + 2, function(err) { err && console.log('addNode.write.meta', err); Fs.write(self.fd, buffer, 0, buffer.length, offset + self.header.pagesize + ((documents - 1) * self.header.documentsize), function(err) { err && console.log('addNode.write.data', err); // type (1b) = from: 0 // index (1b) = from: 1 // state (1b) = from: 2 // pageindex (4b) = from: 3 // continuerindex (4b) = from: 7 // parentindex (4b) = from: 11 // size/count (2b) = from: 15 // data = from: 17 // We must create a new page if (documents + 1 > self.header.pagelimit) { addPage(self, meta.typeid, meta.type.index, meta.type.pageindex, function(err, index) { var documentindex = getDocumentIndex(self, meta.type.pageindex, documents); meta.type.documentindex = documentindex; meta.type.pageindex = index; meta.type.locked = false; // Problem with classes // meta.type.index = 0; if (meta.type.private) self.header.relationpageindex = index; updMeta(self, meta.type.private ? META_RELATIONPAGEINDEX : META_CLASSESRELATIONS); callback(null, documentindex, index); }); } else { var documentindex = getDocumentIndex(self, meta.type.pageindex, documents); meta.type.locked = false; meta.type.documentindex = documentindex; callback(null, documentindex, meta.type.pageindex); } }); }); }); } function addDocument(self, cls, value, callback) { // meta.typeid (1 CLASS, 2 RELATION) // meta.type (link to type class/relation) // meta.state // meta.parentindex // meta.relationindex // meta.size // meta.data var meta = {}; meta.type = cls; meta.typeid = TYPE_CLASS; meta.state = 1; meta.parentindex = 0; meta.relationindex = 0; meta.data = U.createBuffer(stringifyData(cls.schema, value)); meta.size = meta.data.length; var limit = self.header.documentsize - DATAOFFSET; if (meta.data.length > limit) { Zlib.deflate(meta.data, ZLIBOPTIONS, function(err, buf) { if (err || buf.length > limit) callback(new Error('GraphDB: Data too long'), 0); else { meta.state = STATE_COMPRESSED; meta.data = buf; meta.size = buf.length; addNodeFree(self, meta, callback); } }); } else addNodeFree(self, meta, callback); } function addRelation(self, relation, indexA, indexB, callback) { // Workflow: // Has "A" relation nodes? // Has "B" relation nodes? // Create "A" relation with "B" // Create "B" relation with "A" // Register relation to global relations var tasks = []; var relA = null; var relB = null; var tmprelation = { index: relation.index, pageindex: 0, documentindex: 0, locked: false, private: true }; tasks.push(function(next) { self.read(indexA, function(err, doc, relid) { if (doc) { relA = relid; next(); } else { tasks = null; next = null; callback(new Error('GraphDB: Node (A) "{0}" not exists.'.format(indexA))); } }); }); tasks.push(function(next) { self.read(indexB, function(err, doc, relid) { if (doc) { relB = relid; next(); } else { tasks = null; next = null; callback(new Error('GraphDB: Node (B) "{0}" not exists.'.format(indexB))); } }); }); tasks.push(function(next) { if (relA == 0) { next(); return; } checkRelation(self, relation, relA, indexB, function(err, is) { if (is) { tasks = null; next = null; callback(new Error('GraphDB: Same relation already exists between nodes (A) "{0}" and (B) "{1}".'.format(indexA, indexB))); } else next(); }); }); // Obtaining indexA a relation document tasks.push(function(next) { if (F.isKilled) return; IMPORTATOPERATIONS++; if (relA) next(); else { addRelationDocument(self, relation, indexA, function(err, index) { relA = index; next(); }, true); } }); // Obtaining indexB a relation document tasks.push(function(next) { if (F.isKilled) return; if (relB) next(); else { addRelationDocument(self, relation, indexB, function(err, index) { relB = index; next(); }, true); } }); // Push "indexB" relation to "indexA" tasks.push(function(next) { tmprelation.documentindex = relA; tmprelation.pageindex = self.header.relationpageindex; pushRelationDocument(self, relA, tmprelation, indexB, true, function(err, index) { // Updated relation, document was full if (relA !== index) { relA = index; updDocumentRelation(self, indexA, relA, next); } else next(); }, true); }); tasks.push(function(next) { tmprelation.documentindex = relB; tmprelation.pageindex = self.header.relationpageindex; pushRelationDocument(self, relB, tmprelation, indexA, false, function(err, index) { // Updated relation, document was full if (relB !== index) { relB = index; updDocumentRelation(self, indexB, relB, next); } else next(); }, true); }); tasks.push(function(next) { // console.log('PUSH COMMON', relation.documentindex, indexA); pushRelationDocument(self, relation.documentindex, relation, indexA, true, next); }); tasks.async(function() { IMPORTATOPERATIONS--; // console.log('REL ====', relA, relB); callback(null, true); }); } function remRelation(self, relation, indexA, indexB, callback) { var tasks = []; var relA = null; var relB = null; tasks.push(function(next) { self.read(indexA, function(err, doc, relid) { if (doc) { relA = relid; next(); } else { tasks = null; next = null; callback(new Error('GraphDB: Node (A) "{0}" not exists.'.format(indexA))); } }); }); tasks.push(function(next) { self.read(indexB, function(err, doc, relid) { if (doc) { relB = relid; next(); } else { tasks = null; next = null; callback(new Error('GraphDB: Node (B) "{0}" not exists.'.format(indexB))); } }); }); tasks.async(function() { if (F.isKilled) return; IMPORTATOPERATIONS++; remRelationLink(self, relA, indexB, function(err, countA) { remRelationLink(self, relB, indexA, function(err, countB) { remRelationLink(self, relation.documentindex, indexA, function(err, countC) { IMPORTATOPERATIONS--; callback(null, (countA + countB + countC) > 1); }); }); }); }); } function remRelationLink(self, index, documentindex, callback, nochild, counter) { var buf = U.createBufferSize(self.header.documentsize); var offset = offsetDocument(self, index); !counter && (counter = 0); Fs.read(self.fd, buf, 0, buf.length, offset, function() { // type (1b) = from: 0 // index (1b) = from: 1 // state (1b) = from: 2 // pageindex (4b) = from: 3 // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) // parentindex (4b) = from: 11 // size/count (2b) = from: 15 // data = from: 17 if ((buf[0] !== TYPE_RELATION && buf[0] !== TYPE_RELATION_DOCUMENT) || (buf[2] === STATE_REMOVED)) { callback(null, counter); return; } var relid = buf.readUInt32LE(7); var count = buf.readUInt16LE(15); var arr = []; var is = false; for (var i = 0; i < count; i++) { var off = DATAOFFSET + (i * 6); var obj = {}; obj.INDEX = buf[off]; obj.INIT = buf[off + 1]; obj.ID = buf.readUInt32LE(off + 2); if (obj.ID === documentindex && obj.INIT === 1) is = true; else arr.push(obj); } if (is) { count = arr.length; for (var i = 0; i < count; i++) { var off = DATAOFFSET + (i * 6); var obj = arr[i]; buf.writeUInt8(obj.INDEX, off); buf.writeUInt8(obj.INIT, off + 1); buf.writeUInt32LE(obj.ID, off + 2); } buf.writeUInt16LE(count, 15); buf.fill(EMPTYBUFFER, DATAOFFSET + ((count + 1) * 6)); Fs.write(self.fd, buf, 0, buf.length, offset, function() { counter++; if (relid && !nochild) setImmediate(remRelationLink, self, relid, documentindex, callback, null, counter); else callback(null, counter); }); } else if (relid && !nochild) setImmediate(remRelationLink, self, relid, documentindex, callback, null, counter); else callback(null, counter); }); } // Traverses all RELATIONS documents and remove specific "documentindex" function remRelationAll(self, index, documentindex, callback, counter) { var buf = U.createBufferSize(self.header.pagelimit * self.header.documentsize); var offset = offsetDocument(self, index); !counter && (counter = 0); Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { if (err || !size) { callback(null, counter); return; } // type (1b) = from: 0 // index (1b) = from: 1 // state (1b) = from: 2 // pageindex (4b) = from: 3 // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) // parentindex (4b) = from: 11 // size/count (2b) = from: 15 // data = from: 17 var removed = []; while (true) { if (!buf.length) break; index++; var data = buf.slice(0, self.header.documentsize); if ((data[0] !== TYPE_RELATION && data[0] !== TYPE_RELATION_DOCUMENT) || (data[2] === STATE_REMOVED)) { buf = buf.slice(self.header.documentsize); continue; } var count = data.readUInt16LE(15); var arr = []; var is = false; for (var i = 0; i < count; i++) { var off = DATAOFFSET + (i * 6); var obj = {}; obj.INDEX = data[off]; obj.INIT = data[off + 1]; obj.ID = data.readUInt32LE(off + 2); if (obj.ID === documentindex) is = true; else arr.push(obj); } if (is) { var newcount = arr.length; for (var i = 0; i < newcount; i++) { var off = DATAOFFSET + (i * 6); var obj = arr[i]; data.writeUInt8(obj.INDEX, off); data.writeUInt8(obj.INIT, off + 1); data.writeUInt32LE(obj.ID, off + 2); } data.writeUInt16LE(newcount, 15); data.fill(EMPTYBUFFER, DATAOFFSET + ((newcount + 1) * 6)); removed.push({ index: index - 1, buf: data }); } buf = buf.slice(self.header.documentsize); } if (!removed.length) { setImmediate(remRelationAll, self, index, documentindex, callback, counter); return; } counter += removed.length; removed.wait(function(item, next) { Fs.write(self.fd, item.buf, 0, item.buf.length, offsetDocument(self, item.index), next); }, function() { setImmediate(remRelationAll, self, index, documentindex, callback, counter); }); }); } function addRelationDocument(self, relation, index, callback, between) { // meta.typeid (1 CLASS, 2 RELATION, 3 PRIVATE RELATION) // meta.type (link to type class/relation) // meta.state // meta.parentindex // meta.relationindex // meta.size // meta.data var meta = {}; meta.typeid = between ? TYPE_RELATION_DOCUMENT : TYPE_RELATION; meta.type = between ? { index: 0, pageindex: self.header.relationpageindex, documentindex: index, locked: false, private: true } : relation; meta.state = 1; meta.parentindex = 0; meta.relationindex = 0; meta.size = 0; // Creates a new node addNode(self, meta, function(err, relationindex) { // Updates exiting document by updating relation index updDocumentRelation(self, index, relationindex, function(err) { // Returns a new relation index callback(err, relationindex); }); }); } function findDocumentFree(self, pageindex, callback, ready) { var offset = offsetPage(self, pageindex); var buf = U.createBufferSize(self.header.pagesize); Fs.read(self.fd, buf, 0, buf.length, offset, function() { // ==== DB:PAGE (20b) // type (1b) = from: 0 // index (1b) = from: 1 // documents (2b) = from: 2 // freeslots (1b) = from: 4 // parentindex (4b) = from: 5 var relid = buf.readUInt32LE(5); if (!relid) { if (!ready) { callback(null, 0); return; } } // First page is the last page saved in meta therefore is needed to perform recursive with "ready" if (!ready) { findDocumentFree(self, relid, callback, true); return; } var documents = buf.readUInt16LE(2); if (documents >= self.header.pagelimit) { // Finds in parent if exists if (relid) findDocumentFree(self, relid, callback, true); else callback(null, 0); return; } // Finds a free document slot var index = getDocumentIndex(self, pageindex) - 1; var buffer = U.createBufferSize(self.header.pagelimit * self.header.documentsize); Fs.read(self.fd, buffer, 0, buffer.length, offset + self.header.pagesize, function() { while (true) { index++; var data = buffer.slice(0, self.header.documentsize); if (!data.length) break; if (data[2] === STATE_REMOVED) { if (F.isKilled) return; updPageMeta(self, pageindex, function(err, buf) { buf.writeUInt16LE(documents + 1, 2); setImmediate(callback, null, index, pageindex); }); buffer = buffer.slice(self.header.documentsize); return; } } if (relid) findDocumentFree(self, relid, callback, true); else callback(null, 0); }); }); } // Finds a free space for new relation in "pushRelationDocument" function findRelationDocument(self, relid, callback) { if (!relid) { callback(null, 0); return; } var offset = offsetDocument(self, relid); var buf = U.createBufferSize(self.header.documentsize); Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { if (err || !size) { callback(err, 0); return; } var count = buf.readUInt16LE(15); if (count + 1 > self.header.relationlimit) { // Checks if the relation index has next relation if (relid === buf.readUInt32LE(7)) return; relid = buf.readUInt32LE(7); if (relid) setImmediate(findRelationDocument, self, relid, callback); else callback(null, 0); } else { // Free space for this relation callback(null, relid); } }); } // Pushs "documentindex" to "index" document (document with all relations) function pushRelationDocument(self, index, relation, documentindex, initializator, callback, between, recovered) { var offset = offsetDocument(self, index); var buf = U.createBufferSize(self.header.documentsize); Fs.read(self.fd, buf, 0, buf.length, offset, function() { // type (1b) = from: 0 // index (1b) = from: 1 // state (1b) = from: 2 // pageindex (4b) = from: 3 // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) // parentindex (4b) = from: 11 // size/count (2b) = from: 15 // data = from: 17 var count = buf.readUInt16LE(15); if (count + 1 > self.header.relationlimit) { findRelationDocument(self, buf.readUInt32LE(7), function(err, newindex) { // Is some relation document exist? if (newindex && !recovered) { pushRelationDocument(self, newindex, relation, documentindex, initializator, callback, between, true); return; } // meta.typeid (1 CLASS, 2 RELATION) // meta.type (link to type class/relation) // meta.state // meta.parentindex // meta.relationindex // meta.size // meta.buffer var meta = {}; meta.typeid = relation.private ? TYPE_RELATION_DOCUMENT : TYPE_RELATION; meta.type = relation; meta.state = STATE_UNCOMPRESSED; meta.parentindex = 0; meta.relationindex = index; meta.size = 0; addNode(self, meta, function(err, docindex, pageindex) { relation.pageindex = pageindex; relation.documentindex = docindex; updDocumentRelation(self, relation.documentindex, index, function() { updDocumentParent(self, index, relation.documentindex, function() { pushRelationDocument(self, relation.documentindex, relation, documentindex, initializator, callback, between); }); }); }); }); } else { var buffer = U.createBufferSize(6); buffer.writeUInt8(relation.index, 0); buffer.writeUInt8(initializator ? 1 : 0, 1); buffer.writeUInt32LE(documentindex, 2); buffer.copy(buf, DATAOFFSET + (count * 6)); buf.writeUInt16LE(count + 1, 15); if (buf[2] === STATE_REMOVED) { // We must update counts of documents in the page meta var pageindex = Math.ceil(index / self.header.pagelimit); updPageMeta(self, pageindex, function(err, buf) { // type (1b) = from: 0 // index (1b) = from: 1 // documents (2b) = from: 2 // freeslots (1b) = from: 4 // parentindex (4b) = from: 5 buf.writeUInt16LE(buf.readUInt16LE(2) + 1, 2); setImmediate(function() { Fs.write(self.fd, buf, 0, buf.length, offset, function(err) { err && self.error(err, 'pushRelationDocument.read.write'); callback(null, index); }); }); }); buf.writeUInt8(STATE_UNCOMPRESSED, 2); } else { // DONE Fs.write(self.fd, buf, 0, buf.length, offset, function(err) { err && self.error(err, 'pushRelationDocument.read.write'); callback(null, index); }); } } }); } function updDocumentRelation(self, index, relationindex, callback) { if (index === relationindex) throw new Error('FET'); var offset = offsetDocument(self, index); var buf = U.createBufferSize(4); buf.writeUInt32LE(relationindex); Fs.write(self.fd, buf, 0, buf.length, offset + 7, callback); } function updDocumentParent(self, index, parentindex, callback) { var offset = offsetDocument(self, index); var buf = U.createBufferSize(4); buf.writeUInt32LE(parentindex); Fs.write(self.fd, buf, 0, buf.length, offset + 11, callback); } function updPageMeta(self, index, fn) { var offset = offsetPage(self, index); var buf = U.createBufferSize(self.header.pagesize); Fs.read(self.fd, buf, 0, buf.length, offset, function() { fn(null, buf); Fs.write(self.fd, buf, 0, buf.length, offset, self.cb_error); }); } function remDocument(self) { if (!self.ready || self.states.remove || !self.pending.remove.length || F.isKilled) return; self.states.remove = true; var doc = self.pending.remove.shift(); IMPORTATOPERATIONS++; remRelationAll(self, doc.id, doc.id, function() { remDocumentAll(self, doc.id, function(err, count) { IMPORTATOPERATIONS--; self.states.remove = false; doc.callback && doc.callback(err, count); setImmediate(self.cb_next, NEXT_REMOVE); }); }); } function remDocumentAll(self, index, callback, count) { var offset = offsetDocument(self, index); var buf = U.createBufferSize(17); // type (1b) = from: 0 // index (1b) = from: 1 // state (1b) = from: 2 // pageindex (4b) = from: 3 // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) // parentindex (4b) = from: 11 // size/count (2b) = from: 15 // data = from: 17 if (!count) count = 0; Fs.read(self.fd, buf, 0, buf.length, offset, function() { var relid = buf.readUInt32LE(7); if (buf[2] === STATE_REMOVED) { if (relid) remDocumentAll(self, relid, callback, count); else callback(null, count); return; } buf.writeUInt8(STATE_REMOVED, 2); buf.writeUInt16LE(0, 15); if (buf[0] === TYPE_CLASS) self.$classes[buf[1]].findfreeslots = true; var pageindex = buf.readUInt32LE(3); Fs.write(self.fd, buf, 0, buf.length, offset, function() { // Updates "documents" in the current page updPageMeta(self, pageindex, function(err, buf) { // type (1b) = from: 0 // index (1b) = from: 1 // documents (2b) = from: 2 // freeslots (1b) = from: 4 // parentindex (4b) = from: 5 var documents = buf.readUInt16LE(2); buf.writeUInt16LE(documents > 0 ? documents - 1 : documents, 2); count++; setImmediate(function() { if (relid) remDocumentAll(self, relid, callback, count); else callback(null, count); }); }); }); }); } function offsetPage(self, index) { return HEADERSIZE + ((index - 1) * (self.header.pagesize + (self.header.pagelimit * self.header.documentsize))); } function offsetDocument(self, index) { var page = Math.ceil(index / self.header.pagelimit); var offset = page * self.header.pagesize; return HEADERSIZE + offset + ((index - 1) * self.header.documentsize); } function getIndexPage(self, offset) { return ((offset - HEADERSIZE) / (self.header.pagesize + (self.header.pagelimit * self.header.documentsize))); } function getDocumentIndex(self, pageindex, count) { return ((pageindex - 1) * self.header.pagelimit) + (count || 1); } function checkRelation(self, relation, indexA, indexB, callback) { self.read(indexA, function(err, docs, relid) { if (docs) { for (var i = 0; i < docs.length; i++) { var doc = docs[i]; if (doc.ID === indexB && (relation.both || doc.INIT)) { callback(null, true); return; } } } if (relid) setImmediate(checkRelation, self, relation, relid, indexB, callback); else callback(null, false); }); } function updMeta(self, type) { var buf; switch (type) { case META_PAGE_ADD: buf = U.createBufferSize(4); buf.writeUInt32LE(self.header.pages); Fs.write(self.fd, buf, 0, buf.length, 31, self.cb_error); break; case META_PAGE_ADD3: buf = U.createBufferSize(4); buf.writeUInt32LE(self.header.pages, 0); Fs.write(self.fd, buf, 0, buf.length, 31, function() { buf.writeUInt32LE(self.header.relationpageindex, 0); Fs.write(self.fd, buf, 0, buf.length, 47, self.cb_error); }); break; case META_RELATIONPAGEINDEX: buf = U.createBufferSize(4); buf.writeUInt32LE(self.header.relationpageindex, 0); Fs.write(self.fd, buf, 0, buf.length, 47, self.cb_error); break; case META_CLASSESRELATIONS: var obj = {}; obj.c = []; // classes obj.r = []; // relations for (var i = 0; i < self.header.classindex; i++) { var item = self.$classes[i + 1]; obj.c.push({ n: item.name, i: item.index, p: item.pageindex, r: item.schema.raw, d: item.documentindex }); } for (var i = 0; i < self.header.relationindex; i++) { var item = self.$relations[i + 1]; obj.r.push({ n: item.name, i: item.index, p: item.pageindex, b: item.both ? 1 :0, d: item.documentindex }); } buf = U.createBufferSize(HEADERSIZE - 45); buf.writeUInt8(self.header.classindex, 0); buf.writeUInt8(self.header.relationindex, 1); buf.writeUInt32LE(self.header.relationpageindex, 2); buf.write(JSON.stringify(obj), 6); Fs.write(self.fd, buf, 0, buf.length, 45, self.cb_error); break; } } function insDocument(self) { if (!self.ready || self.states.insert || !self.pending.insert.length || F.isKilled) return; var doc = self.pending.insert.shift(); if (doc) { var cls = self.$classes[doc.name]; if (cls == null) { doc.callback(new Error('GraphDB: Class "{0}" not found.'.format(doc.name))); return; } if (cls.locked || !cls.ready) { self.pending.insert.push(doc); setTimeout(self.cb_next, DELAY, NEXT_INSERT); return; } self.states.insert = true; addDocument(self, cls, doc.value, function(err, id) { // setTimeout(insDocument, 100, self); self.states.insert = false; setImmediate(insDocument, self); doc.callback(err, id); }); } } function updDocument(self) { if (!self.ready || self.states.update || !self.pending.update.length || F.isKilled) return; var upd = self.pending.update.shift(); if (upd) { self.states.update = true; var offset = offsetDocument(self, upd.id); var buf = U.createBufferSize(self.header.documentsize); Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { if (err) { self.states.update = false; upd.callback(err); setImmediate(self.cb_next, NEXT_UPDATE); return; } if (!size) { upd.callback(null, 0); self.states.update = false; setImmediate(self.cb_next, NEXT_UPDATE); return; } var save = function(err) { self.states.update = false; !err && Fs.write(self.fd, buf, 0, buf.length, offset, self.cb_error); upd.callback(err, err ? 0 : 1); setImmediate(self.cb_next, NEXT_UPDATE); }; var data = buf.slice(DATAOFFSET, buf.readUInt16LE(15) + DATAOFFSET); var limit = self.header.documentsize - DATAOFFSET; var schema = self.$classes[buf[1]].schema; var doc; if (buf[2] === STATE_COMPRESSED) { Zlib.inflate(data, ZLIBOPTIONS, function(err, buffer) { doc = parseData(schema, buffer.toString('utf8').split('|')); buffer = U.createBuffer(stringifyData(schema, upd.fn(doc, upd.value))); if (buffer.length > limit) { Zlib.deflate(buffer, ZLIBOPTIONS, function(err, buffer) { if (buffer.length <= limit) { buf.writeUInt16LE(buffer.length, 15); buf.writeUInt8(STATE_COMPRESSED, 2); buffer.copy(buf, DATAOFFSET); save(); } else save(new Error('GraphDB: Data too long')); }); } else { buf.writeUInt16LE(buffer.length, 15); buf.writeUInt8(STATE_UNCOMPRESSED, 2); buffer.copy(buf, DATAOFFSET); save(); } }); } else { doc = parseData(schema, data.toString('utf8').split('|')); var o = stringifyData(schema, upd.fn(doc, upd.value)); var buffer = U.createBuffer(o); if (buffer.length > limit) { Zlib.deflate(buffer, ZLIBOPTIONS, function(err, buffer) { if (buffer.length <= limit) { buf.writeUInt16LE(buffer.length, 15); buf.writeUInt8(STATE_COMPRESSED, 2); buffer.copy(buf, DATAOFFSET); save(); } else save(new Error('GraphDB: Data too long')); }); } else { buf.writeUInt16LE(buffer.length, 15); buf.writeUInt8(STATE_UNCOMPRESSED, 2); buffer.copy(buf, DATAOFFSET); save(); } } }); } } function insRelation(self) { if (!self.ready || self.states.relation) return; var doc = self.pending.relation.shift(); if (doc) { var rel = self.$relations[doc.name]; if (rel == null) { doc.callback(new Error('GraphDB: Relation "{0}" not found.'.format(doc.name))); return; } if (rel.locked || !rel.ready) { self.pending.relation.push(doc); setTimeout(insRelation, DELAY, self); return; } self.states.relation = true; if (doc.connect) { addRelation(self, rel, doc.indexA, doc.indexB, function(err, id) { self.states.relation = false; doc.callback(err, id); setImmediate(insRelation, self); }); } else { remRelation(self, rel, doc.indexA, doc.indexB, function(err, id) { self.states.relation = false; doc.callback(err, id); setImmediate(insRelation, self); }); } } } GP.create = function(filename, documentsize, callback) { var self = this; Fs.unlink(filename, function() { var buf = U.createBufferSize(HEADERSIZE); buf.write('Total.js GraphDB embedded', 0); buf.writeUInt8(VERSION, 30); // version buf.writeUInt32LE(0, 31); // pages buf.writeUInt16LE(PAGESIZE, 35); // pagesize buf.writeUInt16LE(PAGELIMIT, 37); // pagelimit buf.writeUInt32LE(0, 39); // documents buf.writeUInt16LE(documentsize, 43); // documentsize buf.writeUInt8(0, 45); // classindex buf.writeUInt8(0, 46); // relationindex buf.writeUInt8(0, 47); // relationpageindex buf.write('{"c":[],"r":[]}', 51); // classes and relations Fs.open(filename, 'w', function(err, fd) { Fs.write(fd, buf, 0, buf.length, 0, function(err) { err && self.error(err, 'create'); Fs.close(fd, function() { callback && callback(); }); }); }); }); return self; }; GP.open = function() { var self = this; Fs.stat(self.filename, function(err, stat) { if (err) { // file not found self.create(self.filename, DOCUMENTSIZE, () => self.open()); } else { self.header.size = stat.size; Fs.open(self.filename, 'r+', function(err, fd) { self.fd = fd; err && self.error(err, 'open'); var buf = U.createBufferSize(HEADERSIZE); Fs.read(self.fd, buf, 0, buf.length, 0, function() { self.header.pages = buf.readUInt32LE(31); self.header.pagesize = buf.readUInt16LE(35); self.header.pagelimit = buf.readUInt16LE(37); self.header.documents = buf.readUInt32LE(39); self.header.documentsize = buf.readUInt16LE(43); var size = F.config['graphdb.' + self.name] || DOCUMENTSIZE; if (size > self.header.documentsize) { setTimeout(function() { self.next(NEXT_RESIZE); }, DELAY); } self.header.relationlimit = ((self.header.documentsize - DATAOFFSET) / 6) >> 0; self.header.classindex = buf[45]; self.header.relationindex = buf[46]; self.header.relationpageindex = buf.readUInt32LE(47); var data = buf.slice(51, buf.indexOf(EMPTYBUFFER, 51)).toString('utf8'); var meta = data.parseJSON(true); for (var i = 0; i < meta.c.length; i++) { var item = meta.c[i]; self.class(item.n, item.r, item); } for (var i = 0; i < meta.r.length; i++) { var item = meta.r[i]; self.relation(item.n, item.b === 1, item); } !self.header.relationpageindex && addPage(self, TYPE_RELATION_DOCUMENT, 0, 0, function(err, index) { self.header.relationpageindex = index; }); self.ready = true; self.next(NEXT_READY); }); }); } }); return self; }; GP.next = function(type) { var self = this; var tmp; switch (type) { case NEXT_READY: for (var i = 0; i < self.pending.meta.length; i++) { tmp = self.pending.meta[i]; if (tmp.type === TYPE_CLASS) self.class(tmp.name, tmp.data); else self.relation(tmp.name, tmp.data); } self.emit('ready'); break; case NEXT_RESIZE: clearTimeout(self.$resizedelay); self.$resizedelay = setTimeout(function() { if (!self.states.resize) { self.ready = false; self.states.resize = true; var size = (F.config['graphdb.' + self.name] || DOCUMENTSIZE); var meta = { documentsize: size > self.header.documentsize ? size : self.header.documentsize }; var keys = Object.keys(self.$classes); for (var i = 0; i < keys.length; i++) { var key = keys[i]; var cls = self.$classes[key]; if (cls.$resize) { !meta.classes && (meta.classes = {}); meta.classes[cls.index] = cls.$resize; cls.$resize = null; } } self.resize(meta, function() { self.states.resize = false; self.ready = true; setImmediate(self.cb_next, NEXT_CONTINUE); }); } }, DELAY); break; case NEXT_INSERT: insDocument(self); break; case NEXT_RELATION: insRelation(self); break; case NEXT_UPDATE: updDocument(self); break; case NEXT_REMOVE: remDocument(self); break; case NEXT_FIND: if (self.pending.find.length) { tmp = self.pending.find.shift(); $find(self, tmp.name, tmp.builder, tmp.reverse); } break; } }; GP.class = function(name, meta, data) { var self = this; if (!self.ready && !data) { self.pending.meta.push({ name: name, data: meta, type: 1 }); return self; } var item = self.$classes[name]; var save = false; if (item == null) { item = {}; item.locked = false; if (data) { item.ready = true; item.name = name; item.index = data.i; item.pageindex = data.p; item.documentindex = data.d; item.findfreeslots = true; } else { self.header.classindex++; item.name = name; item.index = self.header.classindex; item.ready = false; item.pageindex = addPage(self, TYPE_CLASS, item.index, 0, function() { item.ready = true; }); item.documentindex = getDocumentIndex(self, item.pageindex); save = true; } item.schema = parseSchema(meta); self.$classes[item.name] = self.$classes[item.index] = item; } else { var newschema = parseSchema(meta); var raw = item.schema.raw; if (raw !== newschema.raw) { item.$resize = newschema; self.next(NEXT_RESIZE); } } save && updMeta(self, META_CLASSESRELATIONS); return self; }; GP.relation = function(name, both, data) { var self = this; if (!self.ready && !data) { self.pending.meta.push({ name: name, data: both, type: 2 }); return self; } var self = this; var item = self.$relations[name]; var save = false; if (item == null) { item = {}; item.ready = true; item.locked = false; if (data) { item.name = name; item.index = data.i; item.pageindex = data.p; item.documentindex = data.d; item.both = both; } else { self.header.relationindex++; item.name = name; item.index = self.header.relationindex; item.ready = false; item.both = both; item.pageindex = addPage(self, TYPE_RELATION, item.index, 0, function() { item.ready = true; }); item.documentindex = getDocumentIndex(self, item.pageindex); save = true; } self.$relations[item.name] = self.$relations[item.index] = item; } else { // compare } save && updMeta(self, META_CLASSESRELATIONS); return self; }; GP.emit = function(name, a, b, c, d, e, f, g) { var evt = this.$events[name]; if (evt) { var clean = false; for (var i = 0, length = evt.length; i < length; i++) { if (evt[i].$once) clean = true; evt[i].call(this, a, b, c, d, e, f, g); } if (clean) { evt = evt.remove(n => n.$once); if (evt.length) this.$events[name] = evt; else this.$events[name] = undefined; } } return this; }; GP.on = function(name, fn) { if (this.$ready && (name === 'ready' || name === 'load')) { fn(); return this; } if (!fn.$once) this.$free = false; if (this.$events[name]) this.$events[name].push(fn); else this.$events[name] = [fn]; return this; }; GP.once = function(name, fn) { fn.$once = true; return this.on(name, fn); }; GP.removeListener = function(name, fn) { var evt = this.$events[name]; if (evt) { evt = evt.remove(n => n === fn); if (evt.length) this.$events[name] = evt; else this.$events[name] = undefined; } return this; }; GP.removeAllListeners = function(name) { if (name === true) this.$events = EMPTYOBJECT; else if (name) this.$events[name] = undefined; else this.$events[name] = {}; return this; }; GP.resize = function(meta, callback) { // meta.documentsize // meta.classes var self = this; var filename = self.filename + '-tmp'; self.create(filename, meta.documentsize, function(err) { if (err) throw err; Fs.open(filename, 'r+', function(err, fd) { var offset = HEADERSIZE; var newoffset = HEADERSIZE; var size = self.header.pagesize + (self.header.pagelimit * self.header.documentsize); var newsize = self.header.pagesize + (self.header.pagelimit * meta.documentsize); var pageindex = 0; var totaldocuments = 0; var finish = function() { var buf = U.createBufferSize(HEADERSIZE); Fs.read(fd, buf, 0, buf.length, 0, function() { // ==== DB:HEADER (7000b) // name (30b) = from: 0 // version (1b) = from: 30 // pages (4b) = from: 31 // pagesize (2b) = from: 35 // pagelimit (2b) = from: 37 // documents (4b) = from: 39 // documentsize (2b) = from: 43 // classindex (1b) = from: 45 // relationindex (1b) = from: 46 // relationnodeindex = from: 47 // classes + relations = from: 51 // buf. buf.writeUInt32LE(pageindex > 0 ? (pageindex - 1) : 0, 31); buf.writeUInt32LE(totaldocuments, 39); buf.writeUInt16LE(meta.documentsize, 43); var obj = {}; obj.c = []; // classes obj.r = []; // relations for (var i = 0; i < self.header.classindex; i++) { var item = self.$classes[i + 1]; var schema = meta.classes[i + 1]; obj.c.push({ n: item.name, i: item.index, p: item.pageindex, r: schema ? schema.raw : item.schema.raw, d: item.documentindex }); } for (var i = 0; i < self.header.relationindex; i++) { var item = self.$relations[i + 1]; obj.r.push({ n: item.name, i: item.index, p: item.pageindex, b: item.both ? 1 :0, d: item.documentindex }); } buf.writeUInt8(self.header.classindex, 45); buf.writeUInt8(self.header.relationindex, 46); buf.writeUInt32LE(self.header.relationpageindex, 47); buf.write(JSON.stringify(obj), 51); Fs.write(fd, buf, 0, buf.length, 0, function() { // console.log(pageindex, meta.documentsize, totaldocuments); Fs.close(fd, function() { Fs.close(self.fd, function() { Fs.copyFile(self.filename, self.filename.replace(/\.gdb$/, NOW.format('_yyyyMMddHHmm') + '.gdp'), function() { Fs.rename(self.filename + '-tmp', self.filename, function() { callback(null); }); }); }); }); }); }); }; var readvalue = function(docbuf, callback) { var data = docbuf.slice(DATAOFFSET, docbuf.readUInt16LE(15) + DATAOFFSET); if (docbuf[2] === STATE_COMPRESSED) Zlib.inflate(data, ZLIBOPTIONS, (err, data) => callback(data ? data.toString('utf8') : '')); else callback(data.toString('utf8')); }; var writevalue = function(value, callback) { var maxsize = meta.documentsize - DATAOFFSET; var data = U.createBuffer(value); if (data.length > maxsize) { Zlib.deflate(data, ZLIBOPTIONS, (err, data) => callback((!data || data.length > maxsize) ? EMPTYBUFFER : data)); } else callback(data); }; var process = function() { pageindex++; // ==== DB:PAGE (20b) // type (1b) = from: 0 // index (1b) = from: 1 // documents (2b) = from: 2 // freeslots (1b) = from: 4 // parentindex (4b) = from: 5 // ==== DB:DOCUMENT (SIZE) // type (1b) = from: 0 // index (1b) = from: 1 // state (1b) = from: 2 // pageindex (4b) = from: 3 // relationindex (4b) = from: 7 (it's for relations between two documents in TYPE_RELATION page) // parentindex (4b) = from: 11 // size/count (2b) = from: 15 // data = from: 17 var buf = U.createBufferSize(size); Fs.read(self.fd, buf, 0, buf.length, offset, function(err, size) { if (!size) { finish(); return; } var newbuf = U.createBufferSize(newsize); // Copies page info newbuf.fill(buf, 0, self.header.pagesize); buf = buf.slice(self.header.pagesize); var index = self.header.pagesize; var documents = 0; (self.header.pagelimit).async(function(i, next) { // Unexpected problem if (!buf.length) { next(); return; } var docbuf = buf.slice(0, self.header.documentsize); var typeid = docbuf[0]; var indexid = docbuf[1]; if (docbuf[2] !== STATE_REMOVED) { totaldocuments++; documents++; } if (docbuf[2] !== STATE_REMOVED && meta.classes && typeid === TYPE_CLASS && meta.classes[indexid]) { readvalue(docbuf, function(value) { // parseData // stringifyData value = stringifyData(meta.classes[indexid], parseData(self.$classes[indexid].schema, value.split('|'))); writevalue(value, function(value) { if (value === EMPTYBUFFER) { // BIG PROBLEM docbuf.writeUInt16LE(0, 15); docbuf.writeUInt8(STATE_REMOVED, 2); documents--; } else { docbuf.writeUInt16LE(value.length, 15); docbuf.fill(value, DATAOFFSET, DATAOFFSET + value.length); } newbuf.fill(docbuf, index, index + self.header.documentsize); index += meta.documentsize; buf = buf.slice(self.header.documentsize); next(); }); }); } else { newbuf.fill(docbuf, index, index + self.header.documentsize); index += meta.documentsize; buf = buf.slice(self.header.documentsize); next(); } }, function() { // Update count of documents if (newbuf.readUInt16LE(2) !== documents) newbuf.writeUInt16LE(documents, 2); Fs.write(fd, newbuf, 0, newbuf.length, newoffset, function() { offset += size; newoffset += newsize; setImmediate(process); }); }); }); }; process(); }); }); return self; }; function $update(doc, value) { return value; } function $modify(doc, value) { var keys = Object.keys(value); for (var i = 0; i < keys.length; i++) { var key = keys[i]; switch (key[0]) { case '+': case '-': case '*': case '/': var tmp = key.substring(1); if (typeof(doc[tmp]) === 'number') { if (key[0] === '+') doc[tmp] += value[key]; else if (key[0] === '-') doc[tmp] -= value[key]; else if (key[0] === '*') doc[tmp] *= value[key]; else if (key[0] === '/') doc[tmp] = doc[tmp] / value[key]; } break; default: if (doc[key] != undefined) doc[key] = value[key]; break; } } return doc; } GP.remove = function(id, callback) { var self = this; var rem = { id: id, callback: callback || NOOP }; self.pending.remove.push(rem); self.next(NEXT_REMOVE); return self; }; GP.update = function(id, value, callback) { var self = this; var upd = { id: id, value: value, fn: typeof(value) === 'function' ? value : $update, callback: callback || NOOP }; self.pending.update.push(upd); self.next(NEXT_UPDATE);