diff --git a/build/app/unisys/common-netmessage-class.js b/build/app/unisys/common-netmessage-class.js index 8d845659..6056319f 100644 --- a/build/app/unisys/common-netmessage-class.js +++ b/build/app/unisys/common-netmessage-class.js @@ -19,325 +19,397 @@ //////////////////////////////////////////////////////////////////////////////// /** MODULE DECLARATIONS *******************************************************/ - const DBG = { send:false, transact:false }; +const DBG = { send: false, transact: false }; - var m_id_counter = 0; - var m_id_prefix = 'PKT'; - var m_transactions = {}; - var m_netsocket = null; - var m_group_id = null; +var m_id_counter = 0; +var m_id_prefix = "PKT"; +var m_transactions = {}; +var m_netsocket = null; +var m_group_id = null; - // constants - const PROMPTS = require('../system/util/prompts'); - const PR = PROMPTS.Pad('PKT'); - const ERR = ":ERR:"; - const ERR_NOT_NETMESG = ERR+PR+"obj does not seem to be a NetMessage"; - const ERR_BAD_PROP = ERR+PR+"property argument must be a string"; - const ERR_ERR_BAD_CSTR = ERR+PR+"constructor args are string, object"; - const ERR_BAD_SOCKET = ERR+PR+"sender object must implement send()"; - const ERR_DUPE_TRANS = ERR+PR+"this packet transaction is already registered!"; - const ERR_NO_GLOB_UADDR = ERR+PR+"packet sending attempted before UADDR is set!"; - const ERR_UNKNOWN_TYPE = ERR+PR+"packet type is unknown:"; - const ERR_NOT_PACKET = ERR+PR+"passed object is not a NetMessage"; - const ERR_UNKNOWN_RMODE = ERR+PR+"packet routine mode is unknown:"; - const KNOWN_TYPES = ['msend','msig','mcall','state']; - const ROUTING_MODE = ['req','res']; +// constants +const PROMPTS = require("../system/util/prompts"); +const PR = PROMPTS.Pad("PKT"); +const ERR = ":ERR:"; +const ERR_NOT_NETMESG = ERR + PR + "obj does not seem to be a NetMessage"; +const ERR_BAD_PROP = ERR + PR + "property argument must be a string"; +const ERR_ERR_BAD_CSTR = ERR + PR + "constructor args are string, object"; +const ERR_BAD_SOCKET = ERR + PR + "sender object must implement send()"; +const ERR_DUPE_TRANS = + ERR + PR + "this packet transaction is already registered!"; +const ERR_NO_GLOB_UADDR = + ERR + PR + "packet sending attempted before UADDR is set!"; +const ERR_UNKNOWN_TYPE = ERR + PR + "packet type is unknown:"; +const ERR_NOT_PACKET = ERR + PR + "passed object is not a NetMessage"; +const ERR_UNKNOWN_RMODE = ERR + PR + "packet routine mode is unknown:"; +const KNOWN_TYPES = ["msend", "msig", "mcall", "state"]; +const ROUTING_MODE = ["req", "res"]; /// UNISYS NETMESSAGE CLASS /////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ A UNetMessage encapsulates a specific message and data payload for sending across the network. -/*/ class NetMessage { - constructor( msg, data, type ) { - // OPTION 1 - // create NetMessage from (generic object) - if ((typeof msg==='object') && (data===undefined)) { - // make sure it has a msg and data obj - if ((typeof msg.msg!=='string')||(typeof msg.data!=='object')) throw ERR_NOT_NETMESG; - // merge properties into this new class instance and return it - Object.assign(this,msg); - m_SeqIncrement(this); - return this; - } - // OPTION 2 - // create NetMessage from JSON-encoded string - if ((typeof msg==='string') && (data===undefined)) { - let obj = JSON.parse(msg); - Object.assign(this,obj); - m_SeqIncrement(this); - return this; - } - // OPTION 3 - // create new NetMessage from scratch (mesg,data) - // unique id for every NetMessage - if (typeof type==='string') m_CheckType(type); - if ((typeof msg!=='string') || (typeof data!=='object')) throw ERR_ERR_BAD_CSTR; - // allow calls with null data by setting to empty object - this.data = data || {}; - this.msg = msg; - // id and debugging memo support - this.id = this.MakeNewID(); - this.rmode = ROUTING_MODE[0]; // is default 't_req' (trans request) - this.type = type || KNOWN_TYPES[0]; // is default 'msend' (no return) - this.memo = ''; - // transaction support - this.seqnum = 0; // positive when part of transaction - this.seqlog = []; // transaction log - // addressing support - this.s_uaddr = NetMessage.SocketUADDR() || null; // first originating uaddr set by SocketSend() - this.s_group = null; // session groupid is set by external module once validated - this.s_uid = null; // first originating UDATA srcUID - // filtering support - } // constructor +/*/ +class NetMessage { + constructor(msg, data, type) { + // OPTION 1 + // create NetMessage from (generic object) + if (typeof msg === "object" && data === undefined) { + // make sure it has a msg and data obj + if (typeof msg.msg !== "string" || typeof msg.data !== "object") + throw ERR_NOT_NETMESG; + // merge properties into this new class instance and return it + Object.assign(this, msg); + m_SeqIncrement(this); + return this; + } + // OPTION 2 + // create NetMessage from JSON-encoded string + if (typeof msg === "string" && data === undefined) { + let obj = JSON.parse(msg); + Object.assign(this, obj); + m_SeqIncrement(this); + return this; + } + // OPTION 3 + // create new NetMessage from scratch (mesg,data) + // unique id for every NetMessage + if (typeof type === "string") m_CheckType(type); + if (typeof msg !== "string" || typeof data !== "object") + throw ERR_ERR_BAD_CSTR; + // allow calls with null data by setting to empty object + this.data = data || {}; + this.msg = msg; + // id and debugging memo support + this.id = this.MakeNewID(); + this.rmode = ROUTING_MODE[0]; // is default 't_req' (trans request) + this.type = type || KNOWN_TYPES[0]; // is default 'msend' (no return) + this.memo = ""; + // transaction support + this.seqnum = 0; // positive when part of transaction + this.seqlog = []; // transaction log + // addressing support + this.s_uaddr = NetMessage.SocketUADDR() || null; // first originating uaddr set by SocketSend() + this.s_group = null; // session groupid is set by external module once validated + this.s_uid = null; // first originating UDATA srcUID + // filtering support + } // constructor /// ACCESSSOR METHODS /////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ returns the type - /*/ Type() { return this.type } + /*/ + Type() { + return this.type; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ returns true if type matches - /*/ IsType( type ) { return this.type===type } + /*/ + IsType(type) { + return this.type === type; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ returns the type - /*/ SetType( type ) { this.type = m_CheckType(type) } + /*/ + SetType(type) { + this.type = m_CheckType(type); + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ returns the message - /*/ Message() { return this.msg; } + /*/ + Message() { + return this.msg; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ sets the message field - /*/ SetMessage( msgstr ) { this.msg = msgstr; } + /*/ + SetMessage(msgstr) { + this.msg = msgstr; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ returns the entire data payload or the property within the data payload (can return undefined if property doesn't exist) - /*/ Data( prop ) { - if (!prop) return this.data; - if (typeof prop==='string') return this.data[prop]; - throw ERR_BAD_PROP; - } + /*/ + Data(prop) { + if (!prop) return this.data; + if (typeof prop === "string") return this.data[prop]; + throw ERR_BAD_PROP; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ convenience method to set data object entirely - /*/ SetData( propOrVal, val ) { - if (typeof propOrVal==='object') { this.data=propOrVal; return } - if (typeof propOrVal==='string') { this.data[propOrVal]=val; return } - throw ERR_BAD_PROP; - } + /*/ + SetData(propOrVal, val) { + if (typeof propOrVal === "object") { + this.data = propOrVal; + return; + } + if (typeof propOrVal === "string") { + this.data[propOrVal] = val; + return; + } + throw ERR_BAD_PROP; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ returns truthy value (this.data) if the passed msgstr matches the message associated with this NetMessage - /*/ Is( msgstr ) { - return (msgstr===this.msg) ? this.data : undefined; - } + /*/ + Is(msgstr) { + return msgstr === this.msg ? this.data : undefined; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ convenience function return true if server message - /*/ IsServerMessage() { - return this.msg.startsWith('SRV_'); - } + /*/ + IsServerMessage() { + return this.msg.startsWith("SRV_"); + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ getter/setter for the memo description field - /*/ Memo() { return this.memo; } - SetMemo( memo ) { this.memo = memo; } + /*/ + Memo() { + return this.memo; + } + SetMemo(memo) { + this.memo = memo; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ convenience function to return JSON version of this object - /*/ JSON() { - return JSON.stringify(this); - } + /*/ + JSON() { + return JSON.stringify(this); + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ return the session groupid (CLASS-PROJ-HASH) that's been set globally - /*/ SourceGroupID() { return this.s_group } + /*/ SourceGroupID() { + return this.s_group; + } /// TRANSACTION SUPPORT ///////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ The sequence number is positive if this packet is reused - /*/ SeqNum() { - return this.seqnum; - } + /*/ + SeqNum() { + return this.seqnum; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Return the originating address of this netmessage packet. It is valid only after the packet has been sent at least once. - /*/ SourceAddress() { - // is this packet originating from server to a remote? - if (this.s_uaddr===NetMessage.DefaultServerUADDR() && (!this.msg.startsWith('SVR_'))) { - return this.s_uaddr; - } - // this is a regular message forward to remote handlers - return this.IsTransaction() - ? this.seqlog[0] - : this.s_uaddr; - } + /*/ + SourceAddress() { + // is this packet originating from server to a remote? + if ( + this.s_uaddr === NetMessage.DefaultServerUADDR() && + !this.msg.startsWith("SVR_") + ) { + return this.s_uaddr; + } + // this is a regular message forward to remote handlers + return this.IsTransaction() ? this.seqlog[0] : this.s_uaddr; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CopySourceAddress( pkt ) { - if (pkt.constructor.name!=='NetMessage') throw Error(ERR_NOT_PACKET); - this.s_uaddr = pkt.SourceAddress(); - } + CopySourceAddress(pkt) { + if (pkt.constructor.name !== "NetMessage") throw Error(ERR_NOT_PACKET); + this.s_uaddr = pkt.SourceAddress(); + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ return an informational string about the packet useful for logging - /*/ Info( key ) { - switch (key) { - case 'src': /* falls-through */ - default: - return this.SourceGroupID() - ? `${this.SourceAddress()} [${this.SourceGroupID()}]` - : `${this.SourceAddress()}`; - } - } + /*/ + Info(key) { + switch (key) { + case "src": /* falls-through */ + default: + return this.SourceGroupID() + ? `${this.SourceAddress()} [${this.SourceGroupID()}]` + : `${this.SourceAddress()}`; + } + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MakeNewID() { - let idStr = (++m_id_counter).toString(); - this.id = m_id_prefix+idStr.padStart(5,'0'); - return this.id; - } + MakeNewID() { + let idStr = (++m_id_counter).toString(); + this.id = m_id_prefix + idStr.padStart(5, "0"); + return this.id; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Send packet on either provided socket or default socket. Servers provide the socket because it's handling multiple sockets from different clients. - /*/ SocketSend( socket=m_netsocket ) { - this.s_group = NetMessage.GlobalGroupID(); - let dst = socket.UADDR || 'unregistered socket'; - if (!socket) throw Error('SocketSend(sock) requires a valid socket'); - if (DBG.send) { - let status = `sending '${this.Message()}' to ${dst}`; - console.log(PR,status); - } - socket.send(this.JSON()); - // FYI: global m_netsocket is not defined on server, since packets arrive on multiple sockets - } + /*/ + SocketSend(socket = m_netsocket) { + this.s_group = NetMessage.GlobalGroupID(); + let dst = socket.UADDR || "unregistered socket"; + if (!socket) throw Error("SocketSend(sock) requires a valid socket"); + if (DBG.send) { + let status = `sending '${this.Message()}' to ${dst}`; + console.log(PR, status); + } + socket.send(this.JSON()); + // FYI: global m_netsocket is not defined on server, since packets arrive on multiple sockets + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Create a promise to resolve when packet returns - /*/ QueueTransaction( socket=m_netsocket ) { - // global m_netsocket is not defined on server, since packets arrive on multiple sockets - if (!socket) throw Error('QueueTransaction(sock) requires a valid socket'); - // save our current UADDR - this.seqlog.push(NetMessage.UADDR); - let dbg = (DBG.transact)&&(!this.IsServerMessage()); - let p = new Promise((resolve,reject) => { - var hash = m_GetHashKey(this); - if (m_transactions[hash]) { - reject(Error(ERR_DUPE_TRANS+':'+hash)); - } else { - // save the resolve function in transactions table; - // promise will resolve on remote invocation with data - m_transactions[hash] = function (data) { - if (dbg) console.log(PR,'resolving promise with',JSON.stringify(data)); - resolve(data); - }; - this.SocketSend(socket); - } - }); - return p; + /*/ + QueueTransaction(socket = m_netsocket) { + // global m_netsocket is not defined on server, since packets arrive on multiple sockets + if (!socket) throw Error("QueueTransaction(sock) requires a valid socket"); + // save our current UADDR + this.seqlog.push(NetMessage.UADDR); + let dbg = DBG.transact && !this.IsServerMessage(); + let p = new Promise((resolve, reject) => { + var hash = m_GetHashKey(this); + if (m_transactions[hash]) { + reject(Error(ERR_DUPE_TRANS + ":" + hash)); + } else { + // save the resolve function in transactions table; + // promise will resolve on remote invocation with data + m_transactions[hash] = function(data) { + if (dbg) + console.log(PR, "resolving promise with", JSON.stringify(data)); + resolve(data); + }; + this.SocketSend(socket); } + }); + return p; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ return the 'routing mode': req/res is request/reply (message requests and optional response) f_req/f_res is forwarded request/reply (forwarded messages and optional return) the f_res is converted to res and sent back to original requester - /*/ RoutingMode() { return this.rmode; } + /*/ + RoutingMode() { + return this.rmode; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - IsRequest() { return this.rmode==='req'; } - IsOwnResponse() { return this.rmode==='res'; } + IsRequest() { + return this.rmode === "req"; + } + IsOwnResponse() { + return this.rmode === "res"; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ If this packet is a returned transaction, then return true - /*/ IsTransaction() { - return (this.rmode!==ROUTING_MODE[0])&&(this.seqnum>0)&&(this.seqlog[0]===NetMessage.UADDR); - } + /*/ + IsTransaction() { + return ( + this.rmode !== ROUTING_MODE[0] && + this.seqnum > 0 && + this.seqlog[0] === NetMessage.UADDR + ); + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ update the sequence metadata and return on same socket - /*/ ReturnTransaction( socket=m_netsocket ) { - // global m_netsocket is not defined on server, since packets arrive on multiple sockets - if (!socket) throw Error('ReturnTransaction(sock) requires a valid socket'); - // note: seqnum is already incremented by the constructor if this was - // a received packet - // add this to the sequence log - this.seqlog.push(NetMessage.UADDR); - this.rmode = m_CheckRMode('res'); - this.SocketSend(socket); - } + /*/ + ReturnTransaction(socket = m_netsocket) { + // global m_netsocket is not defined on server, since packets arrive on multiple sockets + if (!socket) throw Error("ReturnTransaction(sock) requires a valid socket"); + // note: seqnum is already incremented by the constructor if this was + // a received packet + // add this to the sequence log + this.seqlog.push(NetMessage.UADDR); + this.rmode = m_CheckRMode("res"); + this.SocketSend(socket); + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ If this is a transaction packet that is returned, then execute the stored resolver function from the promise stored in m_transactions, which will then trigger .then() following any calls - /*/ CompleteTransaction() { - let dbg = (DBG.transact) && (!this.IsServerMessage()); - var hash = m_GetHashKey(this); - var resolverFunc = m_transactions[hash]; - if (dbg) console.log(PR,'CompleteTransaction',hash); - if (typeof resolverFunc!=='function') { - throw Error(`transaction [${hash}] resolverFunction is type ${typeof resolverFunc}`); - } else { - resolverFunc(this.data); - Reflect.deleteProperty(m_transactions[hash]); - } - } - } // class NetMessage + /*/ + CompleteTransaction() { + let dbg = DBG.transact && !this.IsServerMessage(); + var hash = m_GetHashKey(this); + var resolverFunc = m_transactions[hash]; + if (dbg) console.log(PR, "CompleteTransaction", hash); + if (typeof resolverFunc !== "function") { + throw Error( + `transaction [${hash}] resolverFunction is type ${typeof resolverFunc}` + ); + } else { + resolverFunc(this.data); + Reflect.deleteProperty(m_transactions[hash]); + } + } +} // class NetMessage /// STATIC CLASS METHODS ////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ set the NETWORK interface object that implements Send() -/*/ NetMessage.GlobalSetup = function( config ) { - let { netsocket, uaddr } = config; - if (uaddr) NetMessage.UADDR = uaddr; - // NOTE: m_netsocket is set only on clients since on server, there are multiple sockets - if (netsocket) { - if (typeof netsocket.send!=='function') throw ERR_BAD_SOCKET; - m_netsocket = netsocket; - } - }; +/*/ +NetMessage.GlobalSetup = function(config) { + let { netsocket, uaddr } = config; + if (uaddr) NetMessage.UADDR = uaddr; + // NOTE: m_netsocket is set only on clients since on server, there are multiple sockets + if (netsocket) { + if (typeof netsocket.send !== "function") throw ERR_BAD_SOCKET; + m_netsocket = netsocket; + } +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ cleanup any allocated storage -/*/ NetMessage.GlobalCleanup = function() { - if (m_netsocket) { - console.log(PR,'GlobalCleanup: deallocating netsocket'); - m_netsocket = null; - } - } +/*/ +NetMessage.GlobalCleanup = function() { + if (m_netsocket) { + console.log(PR, "GlobalCleanup: deallocating netsocket"); + m_netsocket = null; + } +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ return the address (socket_id) assigned to this app instance -/*/ NetMessage.SocketUADDR = function() { - return NetMessage.UADDR; - } +/*/ +NetMessage.SocketUADDR = function() { + return NetMessage.UADDR; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Return a default server UADDR -/*/ NetMessage.DefaultServerUADDR = function() { - return 'SVR_01'; - } +/*/ +NetMessage.DefaultServerUADDR = function() { + return "SVR_01"; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Return current SessionID string -/*/ NetMessage.GlobalGroupID = function () { - return m_group_id; - } +/*/ +NetMessage.GlobalGroupID = function() { + return m_group_id; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NetMessage.GlobalSetGroupID = function ( token ) { - m_group_id = token; - } +NetMessage.GlobalSetGroupID = function(token) { + m_group_id = token; +}; /// PRIVATE CLASS HELPERS ///////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ when a packet is reconstructed from an existing object or json string, its sequence number is incremented, and the old source uaddr is pushed onto the seqlog stack. -/*/ function m_SeqIncrement( pkt ) { - pkt.seqnum++; - return pkt; - } +/*/ +function m_SeqIncrement(pkt) { + pkt.seqnum++; + return pkt; +} /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ return the hash used for storing transaction callbacks -/*/ function m_GetHashKey( pkt ) { - let hash = `${pkt.SourceAddress()}:${pkt.id}`; - return hash; - } +/*/ +function m_GetHashKey(pkt) { + let hash = `${pkt.SourceAddress()}:${pkt.id}`; + return hash; +} /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ is this an allowed type? throw error if not -/*/ function m_CheckType( type ) { - if (type===undefined) throw new Error('must pass a type string, not '+type); - if (!(KNOWN_TYPES.includes(type))) throw `${ERR_UNKNOWN_TYPE} '${type}'`; - return type; - } +/*/ +function m_CheckType(type) { + if (type === undefined) + throw new Error("must pass a type string, not " + type); + if (!KNOWN_TYPES.includes(type)) throw `${ERR_UNKNOWN_TYPE} '${type}'`; + return type; +} /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ is this an allowed mode? throw error if not -/*/ function m_CheckRMode( mode ) { - if (mode===undefined) throw new Error('must pass a mode string, not '+mode); - if (!(ROUTING_MODE.includes(mode))) throw `${ERR_UNKNOWN_RMODE} '${mode}'`; - return mode; - } +/*/ +function m_CheckRMode(mode) { + if (mode === undefined) + throw new Error("must pass a mode string, not " + mode); + if (!ROUTING_MODE.includes(mode)) throw `${ERR_UNKNOWN_RMODE} '${mode}'`; + return mode; +} /// EXPORT CLASS DEFINITION /////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/app/unisys/common-session.js b/build/app/unisys/common-session.js index a0cfa1ef..4ab6c03b 100644 --- a/build/app/unisys/common-session.js +++ b/build/app/unisys/common-session.js @@ -9,95 +9,130 @@ /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const DBG = false; // -const PROMPTS = require('../system/util/prompts'); -const PR = PROMPTS.Pad('SESSUTIL'); +const PROMPTS = require("../system/util/prompts"); +const PR = PROMPTS.Pad("SESSUTIL"); /// SYSTEM LIBRARIES ////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -const HashIds = require('hashids'); +const HashIds = require("hashids"); /// MODULE DEFS /////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -let SESUTIL = {}; -const HASH_ABET = 'ABCDEFGHIJKLMNPQRSTVWXYZ23456789'; +let SESUTIL = {}; +const HASH_ABET = "ABCDEFGHIJKLMNPQRSTVWXYZ23456789"; const HASH_MINLEN = 3; -var m_current_groupid = null; +var m_current_groupid = null; /// SESSION /////////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Given a token of form CLASS-PROJECT-HASHEDID, return an object containing as many decoded values as possible. Check isValid for complete decode succes. groupId is also set if successful -/*/ SESUTIL.DecodeToken = function ( token ) { - if (token===undefined) return {}; - let tokenBits = token.split('-'); - let classId, projId, hashedId, groupId, isValid; - // optimistically set valid flag to be negated on failure - isValid = true; - // token is of form CLS-PRJ-HASHEDID - // classId, etc will be partially set and returned - if (tokenBits[0]) classId = tokenBits[0].toUpperCase(); - if (tokenBits[1]) projId = tokenBits[1].toUpperCase(); - if (tokenBits[2]) hashedId = tokenBits[2].toUpperCase(); - // initialize hashid structure - let salt = `${classId}${projId}`; - let hashids = new HashIds(salt,HASH_MINLEN,HASH_ABET); - // try to decode the groupId - groupId = hashids.decode(hashedId)[0]; - // invalidate if groupId isn't an integer - if (!Number.isInteger(groupId)) { - if (DBG) console.error('invalid token'); - isValid = false; - } - // invalidate if groupId isn't non-negative integer - if (groupId<0) { - if (DBG) console.error('decoded token, but value out of range <0'); - isValid = false; - } - // - let decoded = { isValid, classId, projId, hashedId, groupId }; - return decoded; - }; +/*/ +SESUTIL.DecodeToken = function(token) { + if (token === undefined) return {}; + let tokenBits = token.split("-"); + let classId, projId, hashedId, groupId, subId, isValid; + // optimistically set valid flag to be negated on failure + isValid = true; + // check for superficial issues + if (token.substr(-1) === "-") { + isValid = false; + } + // token is of form CLS-PRJ-HASHEDID + // classId, etc will be partially set and returned + if (tokenBits[0]) classId = tokenBits[0].toUpperCase(); + if (tokenBits[1]) projId = tokenBits[1].toUpperCase(); + if (tokenBits[2]) hashedId = tokenBits[2].toUpperCase(); + if (tokenBits[3]) subId = tokenBits[3].toUpperCase(); + // initialize hashid structure + let salt = `${classId}${projId}`; + let hashids = new HashIds(salt, HASH_MINLEN, HASH_ABET); + // try to decode the groupId + groupId = hashids.decode(hashedId)[0]; + // invalidate if groupId isn't an integer + if (!Number.isInteger(groupId)) { + if (DBG) console.error("invalid token"); + isValid = false; + groupId = 0; + } + // invalidate if groupId isn't non-negative integer + if (groupId < 0) { + if (DBG) console.error("decoded token, but value out of range <0"); + isValid = false; + groupId = 0; + } + + // at this point groupId is valid + // check for valid subgroupId + if (subId) { + if ( + subId.length > 2 && + subId.indexOf("ID") === 0 && + /^\d+$/.test(subId.substring(2)) + ) { + if (DBG) console.log("detected subid", subId.substring(2)); + // subId contains a string "ID" where is an integer + } else { + // subId exists but didn't match subid format + if (DBG) console.log("invalid subId string", subId); + isValid = false; + subId = 0; + } + } + + // if isValid is false, check groupId is 0 or subId is 0, indicating error + let decoded = { isValid, classId, projId, hashedId, groupId, subId }; + return decoded; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Return TRUE if the token decodes into an expected range of values -/*/ SESUTIL.IsValidToken = function ( token ) { - let decoded = SESUTIL.DecodeToken(token); - return (decoded && Number.isInteger(decoded.groupId)); - } +/*/ +SESUTIL.IsValidToken = function(token) { + let decoded = SESUTIL.DecodeToken(token); + return decoded && Number.isInteger(decoded.groupId); +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Returns a token string of form CLASS-PROJECT-HASHEDID classId and projId should be short and are case-insensitive. groupId must be a non-negative integer -/*/ SESUTIL.MakeToken = function ( classId, projId, groupId ) { - // type checking - if (typeof classId!=='string') throw Error(`classId arg1 '${classId}' must be string`); - if (typeof projId!=='string') throw Error(`projId arg2 '${projId}' must be string`); - if (classId.length<1) throw Error(`classId arg1 length should be 1 or more`); - if (projId.length<1) throw Error(`projId arg2 length should be 1 or more`); - if (!Number.isInteger(groupId)) throw Error(`groupId arg3 '${groupId}' must be integer`); - if (groupId<0) throw Error(`groupId arg3 must be non-negative integer`); - if (groupId>Number.MAX_SAFE_INTEGER) throw Error(`groupId arg3 value exceeds MAX_SAFE_INTEGER`); - // initialize hashid structure - classId = classId.toUpperCase(); - projId = projId.toUpperCase(); - let salt = `${classId}${projId}`; - let hashids = new HashIds(salt,HASH_MINLEN,HASH_ABET); - let hashedId = hashids.encode(groupId); - return `${classId}-${projId}-${hashedId}`; - } +/*/ +SESUTIL.MakeToken = function(classId, projId, groupId) { + // type checking + if (typeof classId !== "string") + throw Error(`classId arg1 '${classId}' must be string`); + if (typeof projId !== "string") + throw Error(`projId arg2 '${projId}' must be string`); + if (classId.length < 1) + throw Error(`classId arg1 length should be 1 or more`); + if (projId.length < 1) throw Error(`projId arg2 length should be 1 or more`); + if (!Number.isInteger(groupId)) + throw Error(`groupId arg3 '${groupId}' must be integer`); + if (groupId < 0) throw Error(`groupId arg3 must be non-negative integer`); + if (groupId > Number.MAX_SAFE_INTEGER) + throw Error(`groupId arg3 value exceeds MAX_SAFE_INTEGER`); + // initialize hashid structure + classId = classId.toUpperCase(); + projId = projId.toUpperCase(); + let salt = `${classId}${projId}`; + let hashids = new HashIds(salt, HASH_MINLEN, HASH_ABET); + let hashedId = hashids.encode(groupId); + return `${classId}-${projId}-${hashedId}`; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Set the global GROUPID, which is included in all NetMessage packets that are sent to server. -/*/ SESUTIL.SetGroupID = function ( token ) { - let good = SESUTIL.DecodeToken(token).isValid; - if (good) m_current_groupid = token; - return good; - } +/*/ +SESUTIL.SetGroupID = function(token) { + let good = SESUTIL.DecodeToken(token).isValid; + if (good) m_current_groupid = token; + return good; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SESUTIL.GroupID = function () { - return m_current_groupid; - } +SESUTIL.GroupID = function() { + return m_current_groupid; +}; /// EXPORT MODULE ///////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/app/unisys/component/SessionShell.jsx b/build/app/unisys/component/SessionShell.jsx index 96bbd03c..7ece6ce5 100644 --- a/build/app/unisys/component/SessionShell.jsx +++ b/build/app/unisys/component/SessionShell.jsx @@ -4,162 +4,206 @@ if (window.NC_DBG) console.log(`inc ${module.id}`); SessionShell handles route-based parameters in ReactRouter and updates the SESSION manager with pertinent information + The component stores the credentials + classId : null, + projId : null, + hashedId : null, + groupId : null, + isValid : false + + render() calls one of the following depending on the state of + SESSION.DecodeToken( token ). It returns an object is isValid prop set. + The token is read from this.props.match.params.token, which is provided + by ReactRouter. + + renderLoggedIn( decoded ) contains an object with the decoded properties + from the original string, and displays the login state + + renderLogin() shows the login text field. + + When text is changing in Login Field, this.handleChange() is called. + It gets the value and runs SESSION.DecodeToken() on it. + It then uses Unisys.SetAppState to set "SESSION" to the decoded value. + if a groupId is detected, then it forces a redirect. + + TODO: if an invalid URL is entered, should reset + \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ /// DEBUGGING ///////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -const DBG = true; +const DBG = true; /// LIBRARIES ///////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -const React = require('react'); -const PROMPTS = require('system/util/prompts'); -const SESSION = require('unisys/common-session'); -const PR = PROMPTS.Pad('SessionShell'); -const ReactStrap = require('reactstrap'); -const { Col, - FormGroup, - FormFeedback, - Input, - Label } = ReactStrap; -const { Redirect } = require('react-router-dom') -const UNISYS = require('unisys/client'); +const React = require("react"); +const PROMPTS = require("system/util/prompts"); +const SESSION = require("unisys/common-session"); +const PR = PROMPTS.Pad("SessionShell"); +const ReactStrap = require("reactstrap"); +const { Col, FormGroup, FormFeedback, Input, Label } = ReactStrap; +const { Redirect } = require("react-router-dom"); +const UNISYS = require("unisys/client"); /// CONSTANTS ///////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// these styles are copied from AutoComplete.css const INPUT_STYLE = { - border: '1px solid #aaa', - borderRadius: '4px', - fontFamily: 'Helvetica, sans-serif', + border: "1px solid #aaa", + borderRadius: "4px", + fontFamily: "Helvetica, sans-serif", fontWeight: 300, - fontSize: '10px', - textAlign: 'right', - textTransform: 'uppercase' + fontSize: "10px", + textAlign: "right", + textTransform: "uppercase" }; const GROUP_STYLE = { - backgroundColor: '#777', - color: 'white', - marginTop: '-10px' + backgroundColor: "#777", + color: "white", + marginTop: "-10px" }; const LABEL_STYLE = { - marginBottom: '0.25rem' + verticalAlign: "top", + marginBottom: "0.15rem", + marginTop: "0.15rem" }; /// REACT COMPONENT /////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class SessionShell extends UNISYS.Component { - constructor() { - super(); - this.renderLogin = this.renderLogin.bind(this); - this.renderLoggedIn = this.renderLoggedIn.bind(this); - this.handleChange = this.handleChange.bind(this); - this.state = { - classId : null, - projId : null, - hashedId : null, - groupId : null, - isValid : false - } - } + constructor() { + super(); + this.renderLogin = this.renderLogin.bind(this); + this.renderLoggedIn = this.renderLoggedIn.bind(this); + this.handleChange = this.handleChange.bind(this); + this.state = { + classId: null, + projId: null, + hashedId: null, + groupId: null, + isValid: false + }; + } -/// ROUTE RENDER FUNCTIONS //////////////////////////////////////////////////// -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ render successful logged-in -/*/ renderLoggedIn (decoded) { - if (decoded) { - let classproj = `${decoded.classId}-${decoded.projId}`; - return ( - - - - - - - - - ) - } else { - return

ERROR:renderLoggedIn didn't get valid decoded object

- } - } -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ render must login (readonly) -/*/ renderLogin () { - let { classId, projId, groupId, hashedId, isValid } = this.state; - let formFeedback,tip; - if (classId) tip = "keep entering..."; - else tip = "enter group ID"; - if (hashedId) { - if (hashedId.length>=3) { - if (!isValid) tip=`Invalid code! Check again.`; - } - } - formFeedback = tip ? ( {tip} ) : undefined; + /// ROUTE RENDER FUNCTIONS //////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ render successful logged-in + /*/ + renderLoggedIn(decoded) { + if (decoded) { + let classproj = `${decoded.classId}-${decoded.projId}`; + // prefix with unicode non-breaking space + let gid = `\u00A0${decoded.groupId}`; + let subid = decoded.subId ? `USER\u00A0${decoded.subId}` : ""; return ( - + - + - - - {formFeedback} + + ); + } else { + return

ERROR:renderLoggedIn didn't get valid decoded object

; } -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - componentWillMount() { - // the code below reads a pre-existing matching path, which may be set - // to a valid token string AFTER the changeHandler() detected a valid - // login after a ForceReload. This is a bit hacky and the app would benefit - // from not relying on forced reloads. See handleChange(). - let token = this.props.match.params.token; - let decoded = SESSION.DecodeToken(token) || {}; - this.SetAppState('SESSION',decoded); - } -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ Main Render Function -/*/ render() { - // FUN FACTS - // this.state set in constructor - // this.props.history, location, match added by withRouter(AppShell) - // way back in init-appshell.jsx - let token = this.props.match.params.token; - if (token) { - let decoded = SESSION.DecodeToken(token); - if (decoded.isValid) { - this.AppCall('GROUPID_CHANGE',token); - return this.renderLoggedIn(decoded); - } + } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ render must login (readonly) + /*/ + renderLogin(token) { + let decoded = token ? SESSION.DecodeToken(token) : this.state; + let { classId, projId, groupId, subId, hashedId, isValid } = decoded; + let formFeedback, tip; + if (classId) tip = "keep entering..."; + else tip = "enter group ID"; + if (hashedId) { + if (hashedId.length >= 3) { + if (!isValid) tip = `'${token}' is an invalid code`; } - // failed decode so render login - return this.renderLogin(); } + formFeedback = tip ? ( + + {tip} + + ) : ( + undefined + ); + return ( + + + + + + + {formFeedback} + + + ); + } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + componentWillMount() { + // the code below reads a pre-existing matching path, which may be set + // to a valid token string AFTER the changeHandler() detected a valid + // login after a ForceReload. This is a bit hacky and the app would benefit + // from not relying on forced reloads. See handleChange(). + let token = this.props.match.params.token; + let decoded = SESSION.DecodeToken(token) || {}; + this.SetAppState("SESSION", decoded); + } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ Main Render Function +/*/ render() { + // FUN FACTS + // this.state set in constructor + // this.props.history, location, match added by withRouter(AppShell) + // way back in init-appshell.jsx -/// EVENT HANDLERS //////////////////////////////////////////////////////////// -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - handleChange(event) { - let token = event.target.value; - let decoded = SESSION.DecodeToken(token); - let { classId, projId, hashedId, groupId } = decoded; - this.setState(decoded); - this.SetAppState('SESSION',decoded); - if (decoded.groupId) { - // force a page URL change - let redirect = `/edit/${event.target.value}`; - this.props.history.push(redirect); - } + // no token so just render login + let token = this.props.match.params.token; + if (!token) return this.renderLogin(); + // try to decode token + let decoded = SESSION.DecodeToken(token); + if (decoded.isValid) { + this.AppCall("GROUPID_CHANGE", token); + return this.renderLoggedIn(decoded); } + // error in decode so render login field + return this.renderLogin(token); + } + /// EVENT HANDLERS //////////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + handleChange(event) { + let token = event.target.value; + let decoded = SESSION.DecodeToken(token); + let { classId, projId, hashedId, groupId } = decoded; + this.setState(decoded); + this.SetAppState("SESSION", decoded); + if (decoded.groupId) { + // force a page URL change + let redirect = `/edit/${event.target.value}`; + this.props.history.push(redirect); + } + } } // UNISYS.Component SessionShell - /// EXPORT REACT COMPONENT //////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -module.exports= SessionShell; +module.exports = SessionShell; diff --git a/build/app/unisys/server-database.js b/build/app/unisys/server-database.js index 566e42b6..42e94a0f 100644 --- a/build/app/unisys/server-database.js +++ b/build/app/unisys/server-database.js @@ -4,243 +4,356 @@ DATABASE SERVER \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ -const DBG = false; +const DBG = true; /// LOAD LIBRARIES //////////////////////////////////////////////////////////// /// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = -const Loki = require('lokijs'); -const PATH = require('path'); -const FS = require('fs-extra'); +const Loki = require("lokijs"); +const PATH = require("path"); +const FS = require("fs-extra"); /// CONSTANTS ///////////////////////////////////////////////////////////////// /// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = -const SESSION = require('../unisys/common-session'); -const LOGGER = require('../unisys/server-logger'); -const PROMPTS = require('../system/util/prompts'); -const PR = PROMPTS.Pad('SRV-DB'); -const DB_FILE = './runtime/netcreate.loki'; -const DB_CLONEMASTER = 'alexander.loki'; +const SESSION = require("../unisys/common-session"); +const LOGGER = require("../unisys/server-logger"); +const PROMPTS = require("../system/util/prompts"); +const PR = PROMPTS.Pad("SRV-DB"); +const DB_FILE = "./runtime/netcreate.loki"; +const DB_CLONEMASTER = "alexander.loki"; /// MODULE-WIDE VARS ////////////////////////////////////////////////////////// /// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = -let m_options; // saved initialization options -let m_db; // loki database -let m_max_edgeID; -let m_max_nodeID; -let NODES; // loki "nodes" collection -let EDGES; // loki "edges" collection +let m_options; // saved initialization options +let m_db; // loki database +let m_max_edgeID; +let m_max_nodeID; +let NODES; // loki "nodes" collection +let EDGES; // loki "edges" collection /// API METHODS /////////////////////////////////////////////////////////////// /// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = let DB = {}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ API: Initialize the database -/*/ DB.InitializeDatabase = function( options={} ) { - console.log(PR,`InitializeDatabase`); - FS.ensureDir(PATH.dirname(DB_FILE)); - if (!FS.existsSync(DB_FILE)) { - console.log(PR,`No ${DB_FILE} yet, so filling from ${DB_CLONEMASTER}...`); - FS.copySync(`./runtime/${DB_CLONEMASTER}`,DB_FILE); - console.log(PR,`...success!`); - } - let ropt = { - autoload : true, - autoloadCallback : f_DatabaseInitialize, - autosave : true, - autosaveCallback : f_AutosaveStatus, - autosaveInterval : 4000 // save every four seconds - }; - ropt = Object.assign(ropt,options); - m_db = new Loki(DB_FILE,ropt); - m_options = ropt; - console.log(PR,`Initialized LokiJS Database '${DB_FILE}'`); - - // callback on load - function f_DatabaseInitialize() { - // on the first load of (non-existent database), we will have no - // collections so we can detect the absence of our collections and - // add (and configure) them now. - NODES = m_db.getCollection("nodes"); - if (NODES===null) NODES = m_db.addCollection("nodes"); - EDGES = m_db.getCollection("edges"); - if (EDGES===null) EDGES = m_db.addCollection("edges"); - // find highest NODE ID - if (NODES.count()>0) { - m_max_nodeID = NODES.mapReduce( - (obj) => { return parseInt(obj.id,10) }, - (arr) => { - return Math.max(...arr); - } - ) // end mapReduce node ids - } else { - m_max_nodeID = 0; +/*/ +DB.InitializeDatabase = function(options = {}) { + console.log(PR, `InitializeDatabase`); + FS.ensureDir(PATH.dirname(DB_FILE)); + if (!FS.existsSync(DB_FILE)) { + console.log(PR, `No ${DB_FILE} yet, so filling from ${DB_CLONEMASTER}...`); + FS.copySync(`./runtime/${DB_CLONEMASTER}`, DB_FILE); + console.log(PR, `...success!`); + } + let ropt = { + autoload: true, + autoloadCallback: f_DatabaseInitialize, + autosave: true, + autosaveCallback: f_AutosaveStatus, + autosaveInterval: 4000 // save every four seconds + }; + ropt = Object.assign(ropt, options); + m_db = new Loki(DB_FILE, ropt); + m_options = ropt; + console.log(PR, `Initialized LokiJS Database '${DB_FILE}'`); + + // callback on load + function f_DatabaseInitialize() { + // on the first load of (non-existent database), we will have no + // collections so we can detect the absence of our collections and + // add (and configure) them now. + NODES = m_db.getCollection("nodes"); + if (NODES === null) NODES = m_db.addCollection("nodes"); + EDGES = m_db.getCollection("edges"); + if (EDGES === null) EDGES = m_db.addCollection("edges"); + // find highest NODE ID + if (NODES.count() > 0) { + m_max_nodeID = NODES.mapReduce( + obj => { + return parseInt(obj.id, 10); + }, + arr => { + return Math.max(...arr); } - // find highest EDGE ID - if (EDGES.count()>0) { - m_max_edgeID = EDGES.mapReduce( - (obj) => { return parseInt(obj.id,10) }, - (arr) => { - return Math.max(...arr); - } - ); // end mapReduce edge ids - } else { - m_max_edgeID = 0; + ); // end mapReduce node ids + } else { + m_max_nodeID = 0; + } + // find highest EDGE ID + if (EDGES.count() > 0) { + m_max_edgeID = EDGES.mapReduce( + obj => { + return parseInt(obj.id, 10); + }, + arr => { + return Math.max(...arr); } - console.log(PR,`highest ids: NODE.id='${m_max_nodeID}', EDGE.id='${m_max_edgeID}'`); - } // end f_DatabaseInitialize - + ); // end mapReduce edge ids + } else { + m_max_edgeID = 0; + } + console.log( + PR, + `highest ids: NODE.id='${m_max_nodeID}', EDGE.id='${m_max_edgeID}'` + ); + } // end f_DatabaseInitialize - // UTILITY FUNCTION - function f_AutosaveStatus( ) { - let nodeCount = NODES.count(); - let edgeCount = EDGES.count(); - if (DBG) console.log(PR,`autosaving ${nodeCount} nodes and ${edgeCount} edges...`); - } - }; // InitializeDatabase() + // UTILITY FUNCTION + function f_AutosaveStatus() { + let nodeCount = NODES.count(); + let edgeCount = EDGES.count(); + if (DBG) + console.log( + PR, + `autosaving ${nodeCount} nodes and ${edgeCount} edges...` + ); + } +}; // InitializeDatabase() /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ API: load database note: InitializeDatabase() was already called on system initialization to populate the NODES and EDGES structures. -/*/ DB.PKT_GetDatabase = function ( pkt ) { - let nodes = NODES.chain().data({removeMeta:true}); - let edges = EDGES.chain().data({removeMeta:true}); - if (DBG) console.log(PR,`PKT_GetDatabase ${pkt.Info()} (loaded ${nodes.length} nodes, ${edges.length} edges)`); - LOGGER.Write(pkt.Info(),`getdatabase`); - return { nodes, edges }; - } +/*/ +DB.PKT_GetDatabase = function(pkt) { + let nodes = NODES.chain().data({ removeMeta: true }); + let edges = EDGES.chain().data({ removeMeta: true }); + if (DBG) + console.log( + PR, + `PKT_GetDatabase ${pkt.Info()} (loaded ${nodes.length} nodes, ${ + edges.length + } edges)` + ); + LOGGER.Write(pkt.Info(), `getdatabase`); + return { nodes, edges }; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ API: reset database from scratch -/*/ DB.PKT_SetDatabase = function ( pkt ) { - if (DBG) console.log(PR,`PKT_SetDatabase`); - let { nodes=[], edges=[] } = pkt.Data(); - if (!nodes.length) console.log(PR,'WARNING: empty nodes array'); - else console.log(PR,`setting ${nodes.length} nodes...`); - if (!edges.length) console.log(PR,'WARNING: empty edges array'); - else console.log(PR,`setting ${edges.length} edges...`); - NODES.clear(); NODES.insert(nodes); - EDGES.clear(); EDGES.insert(edges); - console.log(PR,`PKT_SetDatabase complete. Data available on next get.`); - m_db.close(); - DB.InitializeDatabase(); - LOGGER.Write(pkt.Info(),`setdatabase`); - return { OK:true }; - } +/*/ +DB.PKT_SetDatabase = function(pkt) { + if (DBG) console.log(PR, `PKT_SetDatabase`); + let { nodes = [], edges = [] } = pkt.Data(); + if (!nodes.length) console.log(PR, "WARNING: empty nodes array"); + else console.log(PR, `setting ${nodes.length} nodes...`); + if (!edges.length) console.log(PR, "WARNING: empty edges array"); + else console.log(PR, `setting ${edges.length} edges...`); + NODES.clear(); + NODES.insert(nodes); + EDGES.clear(); + EDGES.insert(edges); + console.log(PR, `PKT_SetDatabase complete. Data available on next get.`); + m_db.close(); + DB.InitializeDatabase(); + LOGGER.Write(pkt.Info(), `setdatabase`); + return { OK: true }; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DB.PKT_GetNewNodeID = function ( pkt ) { - m_max_nodeID += 1; - if (DBG) console.log(PR,`PKT_GetNewNodeID ${pkt.Info()} nodeID ${m_max_nodeID}`); - return { nodeID : m_max_nodeID }; - }; +DB.PKT_GetNewNodeID = function(pkt) { + m_max_nodeID += 1; + if (DBG) + console.log(PR, `PKT_GetNewNodeID ${pkt.Info()} nodeID ${m_max_nodeID}`); + return { nodeID: m_max_nodeID }; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DB.PKT_GetNewEdgeID = function ( pkt ) { - m_max_edgeID += 1; - if (DBG) console.log(PR,`PKT_GetNewEdgeID ${pkt.Info()} edgeID ${m_max_edgeID}`); - return { edgeID : m_max_edgeID }; - }; +DB.PKT_GetNewEdgeID = function(pkt) { + m_max_edgeID += 1; + if (DBG) + console.log(PR, `PKT_GetNewEdgeID ${pkt.Info()} edgeID ${m_max_edgeID}`); + return { edgeID: m_max_edgeID }; +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DB.PKT_Update = function ( pkt ) { - let { node, edge, nodeID, replacementNodeID, edgeID } = pkt.Data(); - let retval = {}; - - // PROCESS NODE INSERT/UPDATE - if (node) { - let matches = NODES.find({id:node.id}); - if (matches.length===0) { - // if there was no node, then this is an insert new operation - if (DBG) console.log(PR,`PKT_Update ${pkt.Info()} INSERT nodeID ${JSON.stringify(node)}`); - LOGGER.Write(pkt.Info(),`insert node`,node.id,JSON.stringify(node)); - NODES.insert(node); - retval = { op:'insert', node }; - } else if (matches.length===1) { - // there was one match to update - NODES.findAndUpdate({id:node.id},(n)=>{ - if (DBG) console.log(PR,`PKT_Update ${pkt.Info()} UPDATE nodeID ${node.id} ${JSON.stringify(node)}`); - LOGGER.Write(pkt.Info(),`update node`,node.id,JSON.stringify(node)); - Object.assign(n,node); - }); - retval = { op:'update', node }; - } else { - if (DBG) console.log(PR,`WARNING: multiple nodeID ${node.id} x${matches.length}`); - LOGGER.Write(pkt.Info(),`ERROR`,node.id,'duplicate node id'); - retval = { op:'error-multinodeid' }; - } - return retval; - } // if node - - // PROCESS EDGE INSERT/UPDATE - if (edge) { - let matches = EDGES.find({id:edge.id}); - if (matches.length===0) { - // this is a new edge - if (DBG) console.log(PR,`PKT_Update ${pkt.Info()} INSERT edgeID ${edge.id} ${JSON.stringify(edge)}`); - LOGGER.Write(pkt.Info(),`insert edge`,edge.id,JSON.stringify(edge)); - EDGES.insert(edge); - retval = { op:'insert', edge }; - } else if (matches.length===1) { - // update this edge - EDGES.findAndUpdate({id:edge.id},(e)=>{ - if (DBG) console.log(PR,`PKT_Update ${pkt.SourceGroupID()} UPDATE edgeID ${edge.id} ${JSON.stringify(edge)}`); - LOGGER.Write(pkt.Info(),`update edge`,edge.id,JSON.stringify(edge)); - Object.assign(e,edge); - }); - retval = { op:'update', edge }; - } else { - console.log(PR,`WARNING: multiple edgeID ${edge.id} x${matches.length}`); - LOGGER.Write(pkt.Info(),`ERROR`,node.id,'duplicate edge id'); - retval = { op:'error-multiedgeid' }; - } - return retval; - } // if edge - - // DELETE NODES - if (nodeID !== undefined) { - if (DBG) console.log(PR, `PKT_Update ${pkt.Info()} DELETE nodeID ${nodeID}`); - - // Log first so it's apparent what is triggering the edge changes - LOGGER.Write(pkt.Info(), `delete node`, nodeID); - - // handle edges - let edgesToProcess = EDGES.where((e) => { - return e.source === nodeID || e.target === nodeID; - }); - // `NaN` is not valid JSON, so we use `` - if (replacementNodeID !== '') { - // re-link edges to replacementNodeID - EDGES.findAndUpdate({ source: nodeID }, (e) => { - LOGGER.Write(`...`, pkt.Info(), `relinking edge`, e.id, `to`, replacementNodeID); - e.source = replacementNodeID; - }); - EDGES.findAndUpdate({ target: nodeID }, (e) => { - LOGGER.Write(`...`, pkt.Info(), `relinking edge`, e.id, `to`, replacementNodeID); - e.target = replacementNodeID; - }); - } else { - // delete edges - EDGES.findAndRemove({ source: nodeID }, (e) => { - LOGGER.Write(`...`, pkt.Info(), `deleting edge`, e.id, `from`, nodeID); - e.source = nodeID; - }); - EDGES.findAndRemove({ target: nodeID }, (e) => { - LOGGER.Write(`...`, pkt.Info(), `deleting edge`, e.id, `from`, nodeID); - e.target = nodeID; - }); - } - NODES.findAndRemove({ id: nodeID }); - return { op: 'delete', nodeID, replacementNodeID }; - } - - // DELETE EDGES - if (edgeID!==undefined) { - if (DBG) console.log(PR,`PKT_Update ${pkt.Info()} DELETE edgeID ${edgeID}`); - LOGGER.Write(pkt.Info(),`delete edge`,edgeID); - EDGES.findAndRemove({id:edgeID}); - return { op:'delete',edgeID }; - } - - // return update value - return { op:'error-noaction' }; +DB.PKT_Update = function(pkt) { + let { node, edge, nodeID, replacementNodeID, edgeID } = pkt.Data(); + let retval = {}; + + // PROCESS NODE INSERT/UPDATE + if (node) { + let matches = NODES.find({ id: node.id }); + if (matches.length === 0) { + // if there was no node, then this is an insert new operation + if (DBG) + console.log( + PR, + `PKT_Update ${pkt.Info()} INSERT nodeID ${JSON.stringify(node)}` + ); + LOGGER.Write(pkt.Info(), `insert node`, node.id, JSON.stringify(node)); + DB.AppendNodeLog(node, pkt); // log GroupId to node stored in database + NODES.insert(node); + retval = { op: "insert", node }; + } else if (matches.length === 1) { + // there was one match to update + NODES.findAndUpdate({ id: node.id }, n => { + if (DBG) + console.log( + PR, + `PKT_Update ${pkt.Info()} UPDATE nodeID ${node.id} ${JSON.stringify( + node + )}` + ); + LOGGER.Write(pkt.Info(), `update node`, node.id, JSON.stringify(node)); + DB.AppendNodeLog(n, pkt); // log GroupId to node stored in database + Object.assign(n, node); + }); + retval = { op: "update", node }; + } else { + if (DBG) + console.log( + PR, + `WARNING: multiple nodeID ${node.id} x${matches.length}` + ); + LOGGER.Write(pkt.Info(), `ERROR`, node.id, "duplicate node id"); + retval = { op: "error-multinodeid" }; } + return retval; + } // if node + // PROCESS EDGE INSERT/UPDATE + if (edge) { + let matches = EDGES.find({ id: edge.id }); + if (matches.length === 0) { + // this is a new edge + if (DBG) + console.log( + PR, + `PKT_Update ${pkt.Info()} INSERT edgeID ${edge.id} ${JSON.stringify( + edge + )}` + ); + LOGGER.Write(pkt.Info(), `insert edge`, edge.id, JSON.stringify(edge)); + DB.AppendEdgeLog(edge, pkt); // log GroupId to edge stored in database + EDGES.insert(edge); + retval = { op: "insert", edge }; + } else if (matches.length === 1) { + // update this edge + EDGES.findAndUpdate({ id: edge.id }, e => { + if (DBG) + console.log( + PR, + `PKT_Update ${pkt.SourceGroupID()} UPDATE edgeID ${ + edge.id + } ${JSON.stringify(edge)}` + ); + LOGGER.Write(pkt.Info(), `update edge`, edge.id, JSON.stringify(edge)); + DB.AppendEdgeLog(e, pkt); // log GroupId to edge stored in database + Object.assign(e, edge); + }); + retval = { op: "update", edge }; + } else { + console.log(PR, `WARNING: multiple edgeID ${edge.id} x${matches.length}`); + LOGGER.Write(pkt.Info(), `ERROR`, node.id, "duplicate edge id"); + retval = { op: "error-multiedgeid" }; + } + return retval; + } // if edge + + // DELETE NODES + if (nodeID !== undefined) { + if (DBG) + console.log(PR, `PKT_Update ${pkt.Info()} DELETE nodeID ${nodeID}`); + + // Log first so it's apparent what is triggering the edge changes + LOGGER.Write(pkt.Info(), `delete node`, nodeID); + + // handle edges + let edgesToProcess = EDGES.where(e => { + return e.source === nodeID || e.target === nodeID; + }); + // `NaN` is not valid JSON, so we use `` + if (replacementNodeID !== "") { + // re-link edges to replacementNodeID + EDGES.findAndUpdate({ source: nodeID }, e => { + LOGGER.Write( + `...`, + pkt.Info(), + `relinking edge`, + e.id, + `to`, + replacementNodeID + ); + e.source = replacementNodeID; + }); + EDGES.findAndUpdate({ target: nodeID }, e => { + LOGGER.Write( + `...`, + pkt.Info(), + `relinking edge`, + e.id, + `to`, + replacementNodeID + ); + e.target = replacementNodeID; + }); + } else { + // delete edges + EDGES.findAndRemove({ source: nodeID }, e => { + LOGGER.Write(`...`, pkt.Info(), `deleting edge`, e.id, `from`, nodeID); + e.source = nodeID; + }); + EDGES.findAndRemove({ target: nodeID }, e => { + LOGGER.Write(`...`, pkt.Info(), `deleting edge`, e.id, `from`, nodeID); + e.target = nodeID; + }); + } + NODES.findAndRemove({ id: nodeID }); + return { op: "delete", nodeID, replacementNodeID }; + } + + // DELETE EDGES + if (edgeID !== undefined) { + if (DBG) + console.log(PR, `PKT_Update ${pkt.Info()} DELETE edgeID ${edgeID}`); + LOGGER.Write(pkt.Info(), `delete edge`, edgeID); + EDGES.findAndRemove({ id: edgeID }); + return { op: "delete", edgeID }; + } + + // return update value + return { op: "error-noaction" }; +}; + +/// NODE ANNOTATION /////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ write/remove packet SourceGroupID() information into the node before writing + the first entry is the insert, subsequent operations are updates +/*/ +DB.AppendNodeLog = function(node, pkt) { + if (!node._nlog) node._nlog = []; + let gid = pkt.SourceGroupID() || pkt.SourceAddress(); + node._nlog.push(gid); + if (DBG) { + let out = ""; + node._nlog.forEach(el => { + out += `[${el}] `; + }); + console.log(PR, "nodelog", out); + } +}; +DB.FilterNodeLog = function(node) { + let newNode = Object.assign({}, node); + Reflect.deleteProperty(newNode, "_nlog"); + return newNode; +}; +/// EDGE ANNOTATION /////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ write/remove packet SourceGroupID() information into the node before writing + the first entry is the insert, subsequent operations are updates +/*/ +DB.AppendEdgeLog = function(edge, pkt) { + if (!edge._elog) edge._elog = []; + let gid = pkt.SourceGroupID() || pkt.SourceAddress(); + edge._elog.push(gid); + if (DBG) { + let out = ""; + edge._elog.forEach(el => { + out += `[${el}] `; + }); + console.log(PR, "edgelog", out); + } +}; +DB.FilterEdgeLog = function(edge) { + let newEdge = Object.assign({}, edge); + Reflect.deleteProperty(newEdge, "_elog"); + return newEdge; +}; /// EXPORT MODULE DEFINITION ////////////////////////////////////////////////// /// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = module.exports = DB; diff --git a/build/package.json b/build/package.json index 1bc54ec0..e159587a 100644 --- a/build/package.json +++ b/build/package.json @@ -6,7 +6,8 @@ "bare": "node --inspect brunch-server.js", "dev": "brunch watch -s", "clean": "rm -rf ./public ./node_modules; npm ci", - "debug": "LOGGY_STACKS=true BRUNCH_DEVTOOLS=true ./brunch-debug watch --server" + "debug": "LOGGY_STACKS=true BRUNCH_DEVTOOLS=true ./brunch-debug watch --server", + "logtail": "tail -f -n100 \"$(ls -at ./runtime/logs/* | head -n 1)\"" }, "repository": { "type": "git",