All Downloads are FREE. Search and download functionalities are using the official Maven repository.

0.11.5.source-code.zbus.js Maven / Gradle / Ivy

There is a newer version: 1.0.0-b1
Show newest version
class Protocol{ }
//!!!Javascript message control: xxx_yyy = xxx-yyy (underscore equals dash), both support
//MQ Produce/Consume
Protocol.PRODUCE = "produce";
Protocol.CONSUME = "consume";
Protocol.UNCONSUME = "unconsume";
Protocol.RPC     = "rpc";
Protocol.ROUTE   = "route";     //route back message to sender, designed for RPC

Protocol.SSL = "ssl";

//Topic/ConsumeGroup control
Protocol.DECLARE = "declare";
Protocol.QUERY   = "query";
Protocol.REMOVE  = "remove";
Protocol.EMPTY   = "empty";

//Tracker
Protocol.TRACKER   = "tracker";
Protocol.TRACK_PUB = "track_pub";
Protocol.TRACK_SUB = "track_sub";

Protocol.COMMAND    = "cmd";
Protocol.TOPIC      = "topic";
Protocol.TOPIC_MASK = "topic_mask";
Protocol.TAG        = "tag";
Protocol.OFFSET     = "offset";

Protocol.CONSUME_GROUP      = "consume_group";
Protocol.GROUP_FILTER       = "group_filter";
Protocol.GROUP_MASK         = "group_mask";
Protocol.GROUP_START_COPY   = "group_start_copy";
Protocol.GROUP_START_OFFSET = "group_start_offset";
Protocol.GROUP_START_MSGID  = "group_start_msgid";
Protocol.GROUP_START_TIME   = "group_start_time";

Protocol.WINDOW = "window";

Protocol.SENDER = "sender";
Protocol.RECVER = "recver";
Protocol.ID     = "id";

Protocol.ACK      = "ack";
Protocol.ENCODING = "encoding";

Protocol.ORIGIN_ID     = "origin_id";    
Protocol.ORIGIN_URL    = "origin_url";    
Protocol.ORIGIN_STATUS = "origin_status";  

//Security 
Protocol.TOKEN = "token"; 

Protocol.HEARTBEAT = 'heartbeat'; 

Protocol.MASK_MEMORY    	 = 1<<0;
Protocol.MASK_RPC    	     = 1<<1;
Protocol.MASK_PROXY    	     = 1<<2; 
Protocol.MASK_PAUSE    	     = 1<<3;  
Protocol.MASK_EXCLUSIVE 	 = 1<<4;  
Protocol.MASK_DELETE_ON_EXIT = 1<<5; 

////////////////////////////////////////////////////////////
function hashSize(obj) {
    var size = 0, key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) size++;
    }
    return size;
}

function randomKey (obj) {
    var keys = Object.keys(obj)
    return keys[keys.length * Math.random() << 0];
}

function clone(obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}

function uuid() {
    //http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

String.prototype.format = function () {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function (match, number) {
        return typeof args[number] != 'undefined' ? args[number] : match;
    });
}

Date.prototype.format = function (fmt) {
    var o = {
        "M+": this.getMonth() + 1,
        "d+": this.getDate(),
        "h+": this.getHours(),
        "m+": this.getMinutes(),
        "s+": this.getSeconds(),
        "q+": Math.floor((this.getMonth() + 3) / 3),
        "S": this.getMilliseconds()
    };
    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
    for (var k in o)
        if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    return fmt;
}

function camel2underscore(key) {
    return key.replace(/\.?([A-Z])/g, function (x, y) { return "_" + y.toLowerCase() }).replace(/^_/, "");
}

function underscore2camel(key) {
    return key.replace(/_([a-z])/g, function (g) { return g[1].toUpperCase(); });
} 

var HttpStatus = {
    200: "OK",
    201: "Created",
    202: "Accepted",
    204: "No Content",
    206: "Partial Content",
    301: "Moved Permanently",
    304: "Not Modified",
    400: "Bad Request",
    401: "Unauthorized",
    403: "Forbidden",
    404: "Not Found",
    405: "Method Not Allowed",
    416: "Requested Range Not Satisfiable",
    500: "Internal Server Error"
};

function asleep(ms){
    return new Promise(resolve=>{
        setTimeout(resolve, ms);
    });
}
/////////////////////////logging////////////////////////////

class Logger { 
  constructor(level){
    this.level = level;
    if(!level){
      this.level = Logger.DEBUG;
    }

    this.DEBUG = 0;
    this.INFO = 1;
    this.WARN = 2;
    this.ERROR = 3;  
  } 

  debug() { this._log(this.DEBUG, ...arguments); } 
  info() { this._log(this.INFO, ...arguments); } 
  warn() { this._log(this.WARN, ...arguments); } 
  error() { this._log(this.ERROR, ...arguments); } 
  
  log(level) { this._log(level, ...Array.prototype.slice.call(arguments, 1)); } 

  _log(level) {
    if(level _log => _format  back 2

    if (stackInfo) { 
      var calleeStr = stackInfo.relativePath + ':' + stackInfo.line;
      if (typeof (args[0]) === 'string') {
        args[0] = calleeStr + ' ' + args[0]
      } else {
        args.unshift(calleeStr)
      }
    } 
    return args
  }

  _getStackInfo(stackIndex) { 
    // get all file, method, and line numbers
    var stacklist = (new Error()).stack.split('\n').slice(3)

    // stack trace format: http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
    // do not remove the regex expresses to outside of this method (due to a BUG in node.js)
    var stackReg = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/gi
    var stackReg2 = /at\s+()(.*):(\d*):(\d*)/gi

    var s = stacklist[stackIndex] || stacklist[0]
    var sp = stackReg.exec(s) || stackReg2.exec(s)

    if (sp && sp.length === 5) {
      return {
        method: sp[1],
        relativePath: sp[2].replace(/^.*[\\\/]/, ''),
        line: sp[3],
        pos: sp[4],
        file: sp[2],
        stack: stacklist.join('\n')
      }
    }
  }
}

 
var logger = new Logger(Logger.INFO);  //should export

//////////////////////////////////////////////////////////// 
var __NODEJS__ = typeof module !== 'undefined' && module.exports;

if (__NODEJS__) {

var Events = require('events');
var Buffer = require("buffer").Buffer;
var Socket = require("net");
var inherits = require("util").inherits;
var tls = require("tls");
var fs = require("fs");
    
function string2Uint8Array(str, encoding) {
    var buf = Buffer.from(str, encoding); 
    var data = new Uint8Array(buf.length);
    for (var i = 0; i < buf.length; ++i) {
        data[i] = buf[i];
    }
    return data;
}

function uint8Array2String(data, encoding) {
    var buf = Buffer.from(data);
	return buf.toString(encoding);
} 
    
} else { //WebSocket
	
function inherits(ctor, superCtor) {
	ctor.super_ = superCtor;
	ctor.prototype = Object.create(superCtor.prototype, {
	    constructor: {
	        value: ctor,
	        enumerable: false,
	        writable: true,
	        configurable: true
	    }
	});
};
	
	
function string2Uint8Array(str, encoding) {
	var encoder = new TextEncoder(encoding);
	return encoder.encode(str);
}

function uint8Array2String(buf, encoding) {
	var decoder = new TextDecoder(encoding);
	return decoder.decode(buf);
}

} 

/** 
    * msg.encoding = "utf8"; //encoding on message body
    * msg.body can be 
    * 1) ArrayBuffer, Uint8Array, Init8Array to binary data
    * 2) string value
    * 3) otherwise, JSON converted inside
    * 
    * @param msg
    * @returns ArrayBuffer
    */
function httpEncode(msg) {
    var headers = "";
    var encoding = msg.encoding;
    if (!encoding) encoding = "utf8"; 

    var body = msg.body;
    var contentType = msg["content-type"];
    if (body) {
        if (body instanceof ArrayBuffer) {
            body = new Uint8Array(body);
        } else if (body instanceof Uint8Array || body instanceof Int8Array) {
            //no need to handle
        } else {
            if (typeof body != 'string') {
                body = JSON.stringify(body);
                contentType = "application/json";
            } 
            body = string2Uint8Array(body, encoding);
        }
    } else {
        body = new Uint8Array(0);
    }
    msg["content-length"] = body.byteLength;
    msg["content-type"] = contentType;

    var nonHeaders = { 'status': true, 'method': true, 'url': true, 'body': true }
    var line = ""
    if (msg.status) {
        var desc = HttpStatus[msg.status];
        if (!desc) desc = "Unknown Status";
        line = "HTTP/1.1 {0} {1}\r\n".format(msg.status, desc);
    } else {
        var method = msg.method;
        if (!method) method = "GET";
        var url = msg.url;
        if (!url) url = "/";
        line = "{0} {1} HTTP/1.1\r\n".format(method, url);
    }

    headers += line;
    for (var key in msg) {
        if (key in nonHeaders) continue;
        var val = msg[key];
        if (typeof val == 'undefined' || val == null) continue;
        line = "{0}: {1}\r\n".format(camel2underscore(key), msg[key]);
        headers += line;
    }
    headers += "\r\n";

    delete msg["content-length"]; //clear
    delete msg["content-type"]

    var headerBuffer = string2Uint8Array(headers, encoding);
    var headerLen = headerBuffer.byteLength;
    //merge header and body
    var buffer = new Uint8Array(headerBuffer.byteLength + body.byteLength);  
    for (var i = 0; i < headerBuffer.byteLength; i++) {
        buffer[i] = headerBuffer[i];
    }

    for (var i = 0; i < body.byteLength; i++) {
        buffer[headerLen + i] = body[i];
    }
    return buffer;
};
 
function httpDecode(data) {
    var encoding = "utf8";
    if (typeof data == "string") {
        data = string2Uint8Array(data, encoding);
    } else if (data instanceof Uint8Array || data instanceof Int8Array) {
        //ignore
    } else if (data instanceof ArrayBuffer) {
        data = new Uint8Array(data);
    } else {
        //not support type
        return null;
    }
    var i = 0, pos = -1;
    var CR = 13, NL = 10;
    while (i + 3 < data.byteLength) {
        if (data[i] == CR && data[i + 1] == NL && data[i + 2] == CR && data[i + 3] == NL) {
            pos = i;
            break;
        }
        i++;
    }
    if (pos == -1) return null;

    var str = uint8Array2String(data.slice(0, pos), encoding);

    var blocks = str.split("\r\n");
    var lines = [];
    for (var i in blocks) {
        var line = blocks[i];
        if (line == '') continue;
        lines.push(line);
    }

    var msg = {};
    //parse first line 
    var bb = lines[0].split(" ");
    if (bb[0].toUpperCase().startsWith("HTTP")) {
        msg.status = parseInt(bb[1]);
    } else {
        msg.method = bb[0];
        msg.url = bb[1];
    }
    var typeKey = "content-type";
    var typeVal = "text/plain";
    var lenKey = "content-length";
    var lenVal = 0;

    for (var i = 1; i < lines.length; i++) {
        var line = lines[i];
        var p = line.indexOf(":");
        if (p == -1) continue;
        var key = line.substring(0, p).trim().toLowerCase();
        var val = line.substring(p + 1).trim();
        if (key == lenKey) {
            lenVal = parseInt(val);
            continue;
        }
        if (key == typeKey) {
            typeVal = val;
            continue;
        }

        key = underscore2camel(key);
        msg[key] = val;
    }
    if (pos + 4 + lenVal > data.byteLength) {
        return null;
    }
    //mark consumed Length
    data.consumedLength = pos + 4 + lenVal;

    var encoding = msg.encoding;
    if (!encoding) encoding = "utf8"; 
    var bodyData = data.slice(pos + 4, pos + 4 + lenVal);
    if (typeVal.startsWith("text/html") || typeVal.startsWith("text/plain")) {
        msg.body = uint8Array2String(bodyData, encoding);
    } else if (typeVal.startsWith("application/json")) {
        var bodyString = uint8Array2String(bodyData, encoding);
        msg.body = JSON.parse(bodyString);
    } else {
        msg.body = bodyData;
    }

    return msg;
}
 
function addressKey(serverAddress) {
    var scheme = ""
    if (serverAddress.sslEnabled) {
        scheme = "[SSL]"
    }
    return scheme + serverAddress.address;
}

class ServerAddress {
    constructor(serverAddress, sslEnabled, certificate, token){
        if(typeof(serverAddress) == 'string'){
            serverAddress = serverAddress.trim();
            if (serverAddress.startsWith("ws://")) {
                serverAddress = serverAddress.substring(5);
                sslEnabled = false;
            }
            if (serverAddress.startsWith("wss://")) {
                serverAddress = serverAddress.substring(6);
                sslEnabled = true;
            }

            this.address = serverAddress;
            this.sslEnabled = sslEnabled;
            this.certificate = certificate;
            this.token = token;
            return;
        }  

        this.address = serverAddress.address;
        this.sslEnabled = serverAddress.sslEnabled || sslEnabled==true;
        this.certificate = serverAddress.certificate || certificate;
        this.token = serverAddress.token || token;  
    }

    setCertFile(certFile) {
        if(!__NODEJS__) return; 
        this.certificate = fs.readFileSync(certFile);
    }

    string () {
        var scheme = ""
        if (this.sslEnabled) {
            scheme = "[SSL]"
        }
        return scheme + this.address;
    }

    in (serverAddressArray) {
        for (var i in serverAddressArray) {
            var addr = serverAddressArray[i];
            if (addr.address == this.address && addr.sslEnabled == this.sslEnabled) {
                return true;
            }
        }
        return false;
    } 
}
 
function containsAddress(serverAddressArray, serverAddress) {
    for (var i in serverAddressArray) {
        var addr = serverAddressArray[i];
        if (addr.address == serverAddress.address && addr.sslEnabled == serverAddress.sslEnabled) {
            return true;
        }
    }
    return false;
} 

function MqClient(serverAddress) { 
    if (__NODEJS__) {
        Events.EventEmitter.call(this);
    }
    serverAddress = new ServerAddress(serverAddress); 
    this.serverAddress = serverAddress; 

    var address = this.serverAddress.address;
    var p = address.indexOf(':');
    if (p == -1) {
        this.serverHost = address.trim();
        this.serverPort = 80;
    } else {
        this.serverHost = address.substring(0, p).trim();
        this.serverPort = parseInt(address.substring(p + 1).trim());
    }

    this.autoReconnect = true;
    this.reconnectInterval = 3000;
    this.heartbeatInterval = 60*1000; //60 seconds
    this.ticketTable = {};
    this.onMessage = null;
    this.onConnected = null;
    this.onDisconnected = null;  
    this.socket = null;
    this.connectPromise = null;

    this.readBuf = new Uint8Array(0); //only used when NODEJS 
} 

if (__NODEJS__) {
	
inherits(MqClient, Events.EventEmitter); 

MqClient.prototype.connect = function (connectedHandler) { 
    if(this.socket != null && this.connectPromise != null) {
        return this.connectPromise;
    }
    this.onConnected = connectedHandler;
    //should handle failure 
    var connectSuccess;
    var connectFailure;
    this.connectPromise = new Promise((resolve, reject) => {
        connectSuccess = resolve;
        connectFailure = reject;
    }); 

    try {
        logger.debug("Trying to connect: " + this.serverHost + ":" + this.serverPort); 
        if(this.serverAddress.sslEnabled) {  
            this.socket = tls.connect(this.serverPort, {  
                cert: this.serverAddress.certificate,  
                ca: this.serverAddress.certificate,
            });
        } else {
            this.socket = Socket.connect({ host: this.serverHost, port: this.serverPort }); 
        }  
    } catch (e){
        connectFailure(e);
        return this.connectPromise;
    }
    

    var client = this;
    this.socket.on("connect", function () {
        logger.debug("MqClient connected: " + client.serverHost + ":" + client.serverPort);
        if (connectSuccess) {
            connectSuccess();
        }
        if(client.onConnected){
            client.onConnected(client);
        }
       
        client.heartbeat = setInterval(function () { 
            var msg = {cmd: Protocol.HEARTBEAT};
            try{ client.send(msg); }catch(e){ logger.warn(e); }
        }, client.heartbeatInterval);
    }); 

    this.socket.on("error", function (error) {
        client.emit("error", error);
    });

    this.socket.on("close", function () {
        this.connectPromise = null;
        client.socket.destroy();
        client.socket = null;
        clearInterval(client.heartbeat);
        client.removeAllListeners();

        if (client.onDisconnected) {
            client.onDisconnected(); 
        }  

        if (client.autoReconnect) {
            logger.debug("Trying to recconnect: " + client.serverHost + ":" + client.serverPort); 
            
            client.connectTrying = setTimeout(function () {
                client.connect(connectedHandler);
            }, client.reconnectInterval);
        }
    });

    client.on("error", function (error) {  
        logger.error(error); 
    });

    this.socket.on("data", function (data) { 
        var encoding = "utf8";
        if (typeof data == "string") {
            data = string2Uint8Array(data, encoding);
        } else if (data instanceof Uint8Array || data instanceof Int8Array) {
            //ignore
        } else if (data instanceof ArrayBuffer) {
            data = new Uint8Array(data);
        } else {
            throw "Invalid data type: " + data;
        }
        //concate data
        var bufLen = client.readBuf.byteLength;
        var readBuf = new Uint8Array(bufLen + data.byteLength);
        for (var i = 0; i < bufLen; i++) {
            readBuf[i] = client.readBuf[i];
        }
        for (var i = 0; i < data.byteLength; i++) {
            readBuf[i + bufLen] = data[i];
        }
        client.readBuf = readBuf;

        while (true) {
            if (client.readBuf.consumedLength) {
                client.readBuf = client.readBuf.slice(client.readBuf.consumedLength);
            }
            var msg = httpDecode(client.readBuf);
            if (msg == null) break;
            var msgid = msg.id;
            var ticket = client.ticketTable[msgid];
            if (ticket) { 
                ticket.response = msg;
                if(ticket.resolve){
                    ticket.resolve(msg);
                    delete client.ticketTable[msgid];
                } 
            } else if (client.onMessage) {
                client.onMessage(msg);
            }
        } 
    });

    return this.connectPromise;
}; 

MqClient.prototype.active = function () {
     return this.socket && !this.socket.destroyed;
}

MqClient.prototype.send = function (msg) {
    var buf = httpEncode(msg); 
    this.socket.write(Buffer.from(buf));
}


MqClient.prototype.close = function () {
    this.connectPromise = null;
    if (!this.socket) return;
    clearInterval(this.heartbeat);
    if(this.connectTrying){
        clearTimeout(this.connectTrying);
    } 
    this.socket.removeAllListeners("close");
    this.autoReconnect = false;
    this.socket.destroy();
    this.socket = null;
} 

} else { 
//WebSocket
	
MqClient.prototype.connect = function (connectedHandler) {
    if(this.socket != null && this.connectPromise != null) {
        return this.connectPromise;
    }

    logger.debug("Trying to connect to " + this.address()); 
    this.onConnected = connectedHandler; 

    var connectSuccess;
    var connectFailure;
    this.connectPromise = new Promise((resolve, reject) => {
        connectSuccess = resolve;
        connectFailure = reject;
    }); 

    var WebSocket = window.WebSocket;
    if (!WebSocket) {
        WebSocket = window.MozWebSocket;
    }
    try{
        this.socket = new WebSocket(this.address());
    } catch(e){
        connectFailure(e);
        return this.connectPromise;
    }
    
    this.socket.binaryType = 'arraybuffer';
    var client = this;
    this.socket.onopen = function (event) {
        logger.debug("Connected to " + client.address());
        if (connectSuccess) {
            connectSuccess();
        }
        if(client.onConnected){
            client.onConnected(client);
        }

        client.heartbeat = setInterval(function () {
            var msg = {cmd: Protocol.HEARTBEAT};
            try{ client.send(msg); }catch(e){ logger.warn(e); }
        }, client.heartbeatInterval);
    };

    this.socket.onclose = function (event) {
        client.connectPromise = null;
        clearInterval(client.heartbeat);
        if (client.onDisconnected) {
            client.onDisconnected(); 
        }
        if (client.autoReconnect) {
            client.connectTrying = setTimeout(function () {
                try { client.connect(connectedHandler); } catch (e) { }//ignore
            }, client.reconnectInterval);
        }
    };

    this.socket.onmessage = function (event) {
        var msg = httpDecode(event.data);
        var msgid = msg.id;
        var ticket = client.ticketTable[msgid];
        if (ticket) {
            ticket.response = msg;
            if(ticket.resolve){
                ticket.resolve(msg);
                delete client.ticketTable[msgid];
            } 
        } else if (client.onMessage) {
            client.onMessage(msg);
        }  
    }

    this.socket.onerror = function (data) {
        logger.error("Error: " + data); 
    }
    return this.connectPromise;
}

MqClient.prototype.active = function () {
     return this.socket && this.socket.readyState == WebSocket.OPEN;
}

MqClient.prototype.send = function (msg) {
    var buf = httpEncode(msg);
    this.socket.send(buf);
}

MqClient.prototype.close = function () {
    this.connectPromise = null;
    clearInterval(this.heartbeat);
    if(this.connectTrying){
        clearTimeout(this.connectTrying);
    } 
    this.socket.onclose = function () { }
    this.autoReconnect = false;
    this.socket.close();
    this.socket = null;
}

MqClient.prototype.address = function () {
    if (this.serverAddress.sslEnabled) {
        return "wss://" + this.serverAddress.address;
    }
    return "ws://" + this.serverAddress.address;
}
 
}//End of __NODEJS___

///////////////////////////////commons for Node.JS and Browser///////////////////////////////////
 
MqClient.prototype.fork = function () {
    return new MqClient(this.serverAddress);
} 

function Ticket(reqMsg, resolve) {
    this.id = uuid();
    reqMsg.id = this.id; 
    this.request = reqMsg;
    this.response = null;
    
    this.resolve = resolve;  
} 

MqClient.prototype.invoke = function (msg) {
    var client = this;
    var ticket = new Ticket(msg); 
    this.ticketTable[ticket.id] = ticket;

    var promise = new Promise((resolve, reject)=>{
        if (!client.active()) {
            reject(new Error("socket is not open, invalid"));
            return;
        }  
        ticket.resolve = resolve;
        if(ticket.response){ 
            ticket.resolve(ticket.reponse);
            delete this.ticketTable[ticket.id];
        } 
    });   
    
    this.send(msg); 
    return promise;
};

MqClient.prototype.invokeCmd = function (cmd, msg) {
    if (typeof (msg) == 'string') msg = { topic: msg };  //assume to be topic
    if (!msg) msg = {};

    if (this.token && !msg.token) msg.token = this.token;
    msg.cmd = cmd; 
    return this.invoke(msg);
}

MqClient.prototype.invokeObject = function (cmd, msg) {
    return this.invokeCmd(cmd, msg)
    .then((res)=>{ 
        if(res.status != 200){
            var error = new Error(res.body);
            error.msg = res;
            return {error: error};
        }
        return res.body;
    });
}
  
  
MqClient.prototype.route = function (msg) {
    if (this.token && !msg.token) msg.token = this.token;

    msg.ack = false;
    msg.cmd = Protocol.ROUTE; 
    if (msg.status) {
        msg.originStatus = msg.status;
        delete msg.status;
    }
    this.send(msg);
}

MqClient.prototype.ssl = function (msg) { 
    if(msg.constructor == String){
        msg = { server: msg };
    }
    return this.invokeObject(Protocol.SSL, msg) 
} 

MqClient.prototype.query = function (msg) { 
    return this.invokeObject(Protocol.QUERY, msg);
} 
MqClient.prototype.declare = function (msg) {
    return this.invokeObject(Protocol.DECLARE, msg);
} 
MqClient.prototype.remove = function (msg) {
    return this.invokeCmd(Protocol.REMOVE, msg);
}
MqClient.prototype.empty = function (msg) {
    return this.invokeCmd(Protocol.EMPTY, msg);
}
MqClient.prototype.produce = function (msg) {
    return this.invokeCmd(Protocol.PRODUCE, msg); 
}
MqClient.prototype.consume = function (msg) { 
    return this.invokeCmd(Protocol.CONSUME, msg);
}  
MqClient.prototype.tracker = function () { 
    return this.invokeObject(Protocol.TRACKER, {});
} 


class BrokerRouteTable {
    constructor() {
        this.serverTable = {};  //{ ServerAddress => ServerInfo }
        this.topicTable = {};   //{ TopicName=>{ ServerAddres=>TopicInfo } }
        this.votesTable = {};   //{ TrackerAddress => { servers: Set[ServerAddress], version: xxx, tracker: ServerAddress } }
        this.voteFactor = 0.5;  //for a serverInfo, how much percent of trackers should have voted
        this.votedTrackers = {};
    }
    
    updateTracker (trackerInfo) {
        var trackerFullAddr = addressKey(trackerInfo.serverAddress); 
        this.votedTrackers[trackerFullAddr] = true;
        var toRemove = [];
        //1) Update VotesTable
        var trackedServerList = [];
        for(var serverAddr in trackerInfo.serverTable){
            var serverInfo = trackerInfo.serverTable[serverAddr];
            trackedServerList.push(serverInfo.serverAddress);
        } 
        if (trackerFullAddr in this.votesTable) {
            var vote = this.votesTable[trackerFullAddr];
            if (vote.infoVersion >= trackerInfo.infoVersion) { 
                return toRemove; //No need to update for older version
            }  
            //update votesTable if trackerInfo is newer updates
            vote.infoVersion = trackerInfo.version,
            vote.servers = trackedServerList;
        } else {
            this.votesTable[trackerFullAddr] = {
                version: trackerInfo.infoVersion,
                servers: trackedServerList,
                tracker: trackerInfo.serverAddress,
            };
        }
        
        //2) Merge ServerTable
        var newServerTable = trackerInfo.serverTable;
        for (var serverAddr in newServerTable) {
            var newServerInfo = newServerTable[serverAddr];
            var serverInfo = this.serverTable[serverAddr];
            if (serverInfo && serverInfo.infoVersion >= newServerInfo.infoVersion) {
                continue;
            }
            this.serverTable[serverAddr] = newServerInfo;
        } 
        
        //3) Purge ServerTable
        toRemove = this._purge();
        return toRemove;
    }


    removeTracker (trackerAddress) {
        var trackerFullAddr = addressKey(trackerAddress);
        if (!(trackerFullAddr in this.votesTable)) {
            return [];
        }
        delete this.votesTable[trackerFullAddr];
        return this._purge();
    } 

    _purge() {
        var toRemove = [];
        //Find servers to remove
        for (var serverAddr in this.serverTable) {
            var serverInfo = this.serverTable[serverAddr];
            var serverAddress = serverInfo.serverAddress;
            
            var count = 0;
            for (var trackerAddr in this.votesTable) {//count on votesTable
                var vote = this.votesTable[trackerAddr];
                if (containsAddress(vote.servers, serverAddress)) count++; 
            }
            var totalCount = hashSize(this.votedTrackers); //votedTrackers includes history
            if (count < totalCount * this.voteFactor) {
                toRemove.push(serverAddress);
            }
        }
        //Remove servers
        for (var i in toRemove) {
            var serverAddress = toRemove[i];
            var addrKey = addressKey(serverAddress);
            delete this.serverTable[addrKey];
        }
 
        this.topicTable = {};
        for (var fullAddr in this.serverTable) {
            var serverInfo = this.serverTable[fullAddr]; 
            var serverTopicTable = serverInfo.topicTable;
            for (var topic in serverTopicTable) {
                var serverTopicInfo = serverTopicTable[topic];
                if (!(topic in this.topicTable)) {
                    this.topicTable[topic] = [serverTopicInfo];
                } else {
                    this.topicTable[topic].push(serverTopicInfo);
                }
            }
        }

        return toRemove;
    }
}


class Broker {
    constructor(trackerAddressList, readyTimeout){ 
        this.trackerSubscribers = {};
        this.clientTable = {};
        this.routeTable = new BrokerRouteTable();   

        this.onServerJoin = null;
        this.onServerLeave = null;
        this.onServerUpdated = null;
        this.onTrackerUpdated = null;

        this.readyTable = {};  
        this.readyTriggered = false;
        this.readyTimeout = readyTimeout;
        if(!this.readyTimeout) this.readyTimeout = 3000; //default 3 seconds

        var broker = this;
        this.readyPromise = new Promise(resolve=>{
            broker.onReady = resolve;
        });

        this._addTracker(trackerAddressList); 

        this.readyTimeoutFn = setTimeout(function () {
            if (!broker.readyTriggered) {
                broker.readyTriggered = true;
                if (broker.onReady) {
                    try { broker.onReady(); } catch (e) { logger.debug(e); }
                }
            }
        }, this.readyTimeout);
    }

    close() {
        clearTimeout(this.readyTimeoutFn);
        for(var i in this.clientTable){
            var client = this.clientTable[i];
            client.close();
        }
        this.clientTable = {};
        for(var i in this.trackerSubscribers){
            var sub = this.trackerSubscribers[i];
            sub.close();
        }
        this.trackerSubscribers = {};
    }

    ready() {
        return this.readyPromise;
    }

    select(serverSelector, msg) {
        var keys = serverSelector(this.routeTable, msg);
        var clients = [];
        for (var i in keys) {
            var key = keys[i];
            if (key in this.clientTable) {
                clients.push(this.clientTable[key]);
            }
        }
        return clients;
    }

    addTracker(trackerAddress) {
        trackerAddress = new ServerAddress(trackerAddress);
        var addrKey = trackerAddress.string(); 
        if (addrKey in this.trackerSubscribers){
            var promise = this.readyTable[addrKey];
            return promise;
        } 
        var readyCount = {count: 0, triggered: false};
        var promise = new Promise((resolve, reject)=>{
            readyCount.resolve = resolve;
        }); 

        var client = new MqClient(trackerAddress);
        this.trackerSubscribers[addrKey] = client;

        var broker = this;  
        client.connect(function () {
            var msg = {
                cmd: Protocol.TRACK_SUB,
                token: trackerAddress.token
            }; 
            client.send(msg);
        });

        client.onDisconnected = function(){
            var toRemove = broker.routeTable.removeTracker(client.serverAddress);
            for (var i in toRemove) {
                var serverAddress = toRemove[i];
                broker._removeServer(serverAddress); 
            }
        }

        client.onMessage = function (msg) {
            if (msg.status != 200) {
                logger.warn(msg);
                return;
            }
            var trackerInfo = msg.body;
            if(broker.onTrackerUpdated != null){
                broker.onTrackerUpdated(trackerInfo);
            }
            
            //update remote real address
            var trackerAddress = new ServerAddress(trackerInfo.serverAddress);
            client.serverAddress.address = trackerAddress.address; 

            var addrKey = trackerAddress.string();
            if(!(addrKey in broker.readyTable)) {
                broker.readyTable[addrKey] = readyCount; 
                readyCount.triggered = false;
            }  
            readyCount.count = hashSize(trackerInfo.serverTable);

            var toRemove = broker.routeTable.updateTracker(trackerInfo);
            var serverTable = broker.routeTable.serverTable;
            for (var serverAddr in serverTable) {
                var serverInfo = serverTable[serverAddr];
                broker._addServer(serverInfo, readyCount, trackerAddress);
            }
            for (var i in toRemove) {
                var serverAddress = toRemove[i];
                broker._removeServer(serverAddress); 
            }
            if (broker.onServerUpdated) {
                broker.onServerUpdated();
            }
        }  
    } 

    _addTracker(trackerAddressList){
        if(trackerAddressList){
            if(trackerAddressList.constructor == String){
                var bb = trackerAddressList.split(";");
                for(var i in bb){
                    var trackerAddress = bb[i];
                    if(trackerAddress.trim() == "") continue; 
                    this.addTracker(trackerAddress);
                } 
            } else if(trackerAddressList.constructor == Array){
                for(var i in trackerAddressList){
                    var trackerAddress = trackerAddressList[i];
                    trackerAddress = new ServerAddress(trackerAddress);
                    this.addTracker(trackerAddress);
                }
            } else if(trackerAddressList.constructor == ServerAddress){
                var trackerAddress = trackerAddressList;
                this.addTracker(trackerAddress);
            } else {
                this.addTracker(trackerAddressList);
            }
        } 
    }

    _addServer(serverInfo, readyCount, trackerAddress) {
        var serverAddress = serverInfo.serverAddress;  
        serverAddress = new ServerAddress(serverAddress); 
        if(serverAddress.sslEnabled) {
            var sslClient = new MqClient(trackerAddress);
            var req = {
                server: serverAddress.address,
                token: trackerAddress.token,
            }
            sslClient.connect(()=>{
                sslClient.ssl(req).then(certificate=>{
                    serverAddress.certificate = certificate;
                    this._addServerWithReadyAddress(serverInfo, serverAddress, readyCount);
                }); 
            }) 
        } else {
            this._addServerWithReadyAddress(serverInfo, serverAddress, readyCount);
        } 
    }

    _addServerWithReadyAddress(serverInfo, serverAddress, readyCount) {
        var addrKey = serverAddress.string();  
        var client = this.clientTable[addrKey];
        if (client != null) { 
            return;
        } 
        client = new MqClient(serverAddress);
        this.clientTable[addrKey] = client;  
        var broker = this;

        client.connect(function () {
            if (broker.onServerJoin) {
                logger.info("Server Joined: " + addrKey);
                broker.onServerJoin(serverInfo);
            }  
            
            readyCount.count--;
            if (readyCount.count <= 0) {
                if(readyCount.resolve){
                    readyCount.resolve();
                }
                readyCount.triggered = true;
                if (!broker.readyTriggered) {
                    broker.readyTriggered = true;
                    if (broker.onReady) {
                        try { broker.onReady(); } catch (e) { logger.debug(e); }
                    }
                }
            }
        });  
    }

    _removeServer(serverAddress) { 
        serverAddress = new ServerAddress(serverAddress);
        var addrKey = serverAddress.string();
        var client = this.clientTable[addrKey];
        if (client == null) {
            return;
        }

        if (this.onServerLeave) {
            logger.info("Server Left: " + addrKey);
            this.onServerLeave(serverAddress);
        }

        client.close();
        delete this.clientTable[addrKey]; 
    } 
}

////////////////////////////////////////////////////////// 
class MqAdmin {
    constructor(broker, token){
        this.broker = broker;
        this.adminServerSelector = (routeTable, msg) => {
            return Object.keys(routeTable.serverTable);
        }
        this.token = token;
    }

    invokeCmd0(func, msg, serverSelector) { 
        if(!serverSelector){
            serverSelector = this.adminServerSelector;
        } 
        var clients = this.broker.select(serverSelector, msg);
        if (clients.length < 1) {
            throw new Error("Missing server for topic: " + msg.topic);
        }
        if(msg.constructor == String){
            msg = {topic: msg};
        }
        if(!msg.token) {
            msg.token = this.token;
        }

        if (clients.length == 1) {
            return clients[0][func](msg); 
        }
        var promises = [];
        for (var i in clients) {
            var client = clients[i]; 
            promises.push(client[func](msg));
        }
        return Promise.all(promises);
    }

    invokeCmd(func, msg, serverSelector) {  
        var admin = this;
        if(!this.broker.readyTriggered){
            return this.broker.ready().then(()=>{
                return admin.invokeCmd0(func, msg, serverSelector);
            })
        }
        return this.invokeCmd0(func, msg, serverSelector);
    }

    declare(msg, serverSelector) { 
        return this.invokeCmd(Protocol.DECLARE, msg, serverSelector);
    }
    query (msg, serverSelector) {
        return this.invokeCmd(Protocol.QUERY, msg, serverSelector);
    }
    remove(msg, serverSelector) {
        return this.invokeCmd(Protocol.REMOVE, msg, serverSelector);
    }
    empty(msg, serverSelector) {
        return this.invokeCmd(Protocol.EMPTY, msg, serverSelector);
    }
}
 

class Producer extends(MqAdmin) {
    constructor(broker, token) {
        super(broker, token);
        this.produceServerSelector = (routeTable, msg) => {
            var topic = msg.topic;
            if (hashSize(routeTable.serverTable) == 0) return [];
            var topicList = routeTable.topicTable[topic];
            if (!topicList || topicList.length == 0) { 
                return [randomKey(routeTable.serverTable)]; //random select
            }
            var target = topicList[0];
            for (var i = 1; i < topicList.length; i++) {
                var current = topicList[i];
                if (target.consumerCount < current.consumerCount) {
                    target = current;
                }
            }
            return [addressKey(target.serverAddress)];
        }
    }  

    publish(msg, serverSelector) {
        if(!serverSelector){
            serverSelector = this.produceServerSelector;
        }
        return this.invokeCmd(Protocol.PRODUCE, msg, serverSelector);
    } 
}

 
class Consumer extends MqAdmin {
    constructor(broker, consumeCtrl){
        super(broker, consumeCtrl.token); 

        this.consumeServerSelector = (routeTable, msg) => {
            return Object.keys(routeTable.serverTable);
        }
        this.connectionCount = 1;
        this.messageHandler = null;
        if (consumeCtrl.constructor == String) {
            consumeCtrl = {
                topic: consumeCtrl,
                consume_group: consumeCtrl,
                token: this.token
            };
        }
        this.consumeCtrl = clone(consumeCtrl); 
        if(this.consumeCtrl.consume_group == undefined || this.consumeCtrl.consume_group == null){
            this.consumeCtrl.consume_group = this.consumeCtrl.topic;
        }

        this.consumeClientTable = {}; //addressKey => list of MqClient consuming
    }  

    start() {
        var clientTable = this.broker.clientTable;
        for (var addr in clientTable) {
            var client = clientTable[addr];
            this.consumeToServer(client)
        }
        var consumer = this;
        this.broker.onServerJoin = function (serverInfo) {
            var addr = addressKey(serverInfo.serverAddress);
            var client = consumer.broker.clientTable[addr];
            if (!client) {
                logger.warn(addr + " not found in broker clientTable");
                return;
            }
            consumer.consumeToServer(client);
        }
        this.broker.onServerLeave = function (serverAddress) {
            consumer.leaveServer(serverAddress);
        }
    }
    

    consumeToServer(clientInBrokerTable) {
        var addr = addressKey(clientInBrokerTable.serverAddress);
        if (addr in this.consumeClientTable) return; 
    
        if(!this.messageHandler) {
            throw new Error("Missing messageHandler");
        }

        var clients = []; 
        var consumer = this;
        for (var i = 0; i < this.connectionCount; i++) {
            var client = clientInBrokerTable.fork(); //create new connections
            clients.push(client);   
            client.connect((client) => {  
                consumer.consume(client); 
            });  
        }  
        this.consumeClientTable[addr] = clients; 
    }

    leaveServer(serverAddress) {
        var addr = addressKey(serverAddress);
        var consumingClients = this.consumeClientTable[addr];
        if (!consumingClients) return;
        for (var i in consumingClients) {
            var client = consumingClients[i];
            client.close();
        }
        delete this.consumeClientTable[addr]; 
    } 

    consume(client) {  
        var consumer = this; 
        var ctrl = clone(this.consumeCtrl);
        client.token = this.consumeCtrl.token; //!!client need this token in route message!!

        client.consume(ctrl)
        .then(res => {
            if(res.status == 404) {//Missing topic, to declare 
                var ctrl = clone(consumer.consumeCtrl);
                logger.debug("Trying to declare " + JSON.stringify(ctrl));
                return client.declare(ctrl)
                .then(res=>{
                    if (res.error) { 
                        throw new Error("declare error: " + res.error);
                    } 
                    return consumer.consume(client);
                });
            } 

            var originUrl = res.originUrl;
            var id = res.originId; 
            delete res.originUrl;
            delete res.originId;
            if (typeof originUrl == "undefined") originUrl = "/";  
            res.id = id;
            res.url = originUrl; 
            try {
                consumer.messageHandler(res, client);
            } finally {
                consumer.consume(client);
            } 
        }); 
    }
}

/**
 * Mode1: MQ-based RPC
 * Mode2: Http direct RPC
 */
class RpcInvoker { 
    constructor(brokerOrClient, topicOrCtrl, serverSelector){
        if(serverSelector){
            this.rpcServerSelector = serverSelector;
        }  else if(brokerOrClient.constructor == Broker){
            this.rpcServerSelector = (routeTable, msg) => {
                var topic = msg.topic;
                if (hashSize(routeTable.serverTable) == 0) return [];
                var topicList = routeTable.topicTable[topic];
                if (!topicList || topicList.length == 0) { 
                    return [randomKey(routeTable.serverTable)]; //random select
                }
                var target = topicList[0];
                for (var i = 1; i < topicList.length; i++) {
                    var current = topicList[i];
                    if (target.consumerCount < current.consumerCount) {
                        target = current;
                    }
                }
                return [addressKey(target.serverAddress)];
            }
        } 
        if(brokerOrClient.constructor == Broker){
            this.prodcuer = new Producer(brokerOrClient); 
            if(topicOrCtrl.constructor == String){
                this.ctrl = {topic: topicOrCtrl};
            } else {
                this.ctrl = clone(topicOrCtrl); 
            } 
        } else if(brokerOrClient.constructor == MqClient){
            this.client = brokerOrClient;
            this.ctrl = {};
        } else {
            throw "brokerOrClient should be Broker or MqClient";
        }
        
        var invoker = this; 
        return new Proxy(this, {
            get: function (target, name) {
                return name in target ? target[name] : invoker.proxyMethod(name);
            }
        });
    } 

     //{ method: xxx, params:[], module: xxx}
    invoke(){ 
        if (arguments.length < 1) {
            throw "Missing request parameter";
        }
        var req = arguments[0]; 
        if (typeof (req) == 'string') { 
            var params = [];
            var len = arguments.length; 
            for (var i = 1; i < len; i++) {
                params.push(arguments[i]);
            }
            req = {
                method: req,
                params: params,
            }
        } 
        if(this.module && req && !req.module){
            req.module = this.module;
        }
        return this.invokeMethod(req);
    } 
 
    invokeMethod(req) {
        var msg = clone(this.ctrl);
        msg.body = JSON.stringify(req); 
        
        var p;
        if('prodcuer' in this){
            msg.ack = false; //RPC no need reply from broker
            p = this.prodcuer.publish(msg, this.rpcServerSelector);   
        } else {
            var httpClient = this.client;
            if(httpClient.active()){
                p = httpClient.invokeCmd(null, msg);
            } else {
                p = httpClient.connect().then(function(){
                    return httpClient.invokeCmd(null, msg);
                });
            } 
        } 

        return p.then(msg => {
            if (msg.status != 200) {
                throw msg.body;
            } 
            return msg.body; 
        });
    } 

    proxyMethod(method) {
        var invoker = this;
        return function () {  
            var len = arguments.length; 
            var params = [];
            for (var i = 0; i < len; i++) {
                params.push(arguments[i]);
            }
            var req = {
                method: method,
                params: params,
            }  
            if(this.module){
                req.module = this.module;
            }
            return invoker.invokeMethod(req);
        }
    }
}

class RpcProcessor {
    constructor(){
        this.methodTable = {} 
        var processor = this;
        this.messageHandler = function (msg, client) { 
            var resMsg = {
                id: msg.id,
                recver: msg.sender,
                topic: msg.topic, 
            }; 
            var status = 600; //error
            var result;
            
            try { 
                var req = msg.body;
                if(typeof(req) == 'string') req = JSON.parse(req);  
                var key = processor._generateKey(req.module, req.method);
                var m = processor.methodTable[key];
                if(m) {
                    try {
                        if(!req.params) req.params = [];
                        req.params.push(msg);
                        result = m.method.apply(m.target, req.params);
                        status = 200; //OK 
                    } catch (e) { 
                        result = e; 
                    }  
                } else { 
                    result = "method(" + key + ") not found";
                } 
            } catch (e) {
                result = e; 
            } finally {    
                if(result.__http__){ //specail case for HTTP message response
                    delete result.__http__;
                    result.id = resMsg.id;
                    result.recver = resMsg.recver;
                    result.topic = resMsg.topic;
                    if(!result.status){
                        result.status = 200;
                    }
                    resMsg = result;
                } else {
                    resMsg.status = status;
                    resMsg.body = result;
                }
                
                try { client.route(resMsg); } catch (e) { }
            }
        }
    }

    _generateKey(moduleName, methodName){
        if (moduleName) return moduleName + ":" + methodName;
        return ":" + methodName;
    }

    addModule(serviceObj, moduleName) {
        if (typeof (serviceObj) == 'function') {
            var func = serviceObj;
            var key = this._generateKey(moduleName, func.name);
            if (key in this.methodTable) {
                logger.warn(key + " already exists, will be overriden");
            }
            this.methodTable[key] = { method: func, target: null};
            return;
        }

        for (var name in serviceObj) {
            var func = serviceObj[name];
            if (typeof (func) != 'function') continue;
            var key = this._generateKey(moduleName, name);
            if (key in this.methodTable) {
                logger.warn(key + " already exists, will be overriden");
            }
            this.methodTable[key] = { method: func, target: serviceObj };
        }
    }
}

class ServiceBootstrap {
    constructor(){
        this.processor = new RpcProcessor();
        this.broker = new Broker(); 
        this.connectionSize = 1;
    }  

    start () {
        if(this.consumer) return; 
        if(!this.topic){
            throw new Error("Missing serviceName");
        }  
        var consumeGroup = this.topic;
        if(this.consume_group != null){
            consumeGroup = this.consume_group;
        }
        var consumeCtrl = {
            topic: this.topic,
            consume_group: consumeGroup,
            topic_mask: Protocol.MASK_MEMORY | Protocol.MASK_RPC,
            token: this.token
        }; 
        this.consumer = new Consumer(this.broker, consumeCtrl);
        this.consumer.messageHandler = this.processor.messageHandler;
        this.consumer.connectionCount = this.connectionSize; 
        this.consumer.start(); 
    } 

    close() {
        if(this.consumer){
            this.consumer.close();
            this.consumer = null;
        }
        if(this.broker){
            this.broker.close();
            this.broker = null;
        }
    }

    serviceAddress(address){
        this.broker._addTracker(address);
        return this;
    }
    serviceName(name) {
        this.topic = name;
        return this;
    } 
    serviceToken(token){
        this.token = token;
        return this;
    }   
    connectionCount(count){
        this.connectionSize = count;
        return this;
    } 
    addModule(serviceObj, moduleName){
        this.processor.addModule(serviceObj, moduleName);
        return this;
    } 
}



class ClientBootstrap {
    constructor(){  
        this.broker = null;
        this.httpClient = null;
        this.topic = null;
        this.token = null;
    }   

    serviceAddress(address){
        this.serverAddress = address; //address list also support when in MQ mode 
        return this;
    }

    serviceName(name) {
        this.topic = name;
        return this;
    } 

    serviceToken(token){
        this.token = token;
        return this;
    }   

    invoker(){
        var ctrl = { };
        if(this.token != null){
            ctrl.token = this.token;
        }

        if(this.serverAddress == null){
            if(window != undefined){
                var url = window.location.href;
                var arr = url.split("/");
                this.serverAddress = arr[2];
            } else {
                throw 'serverAddress missing';
            }
        }

        if(this.topic != null) {//MQ mode
            ctrl.topic = this.topic;
            if(this.broker == null){
                this.broker = new Broker();
                this.broker._addTracker(this.serverAddress);
            } 
            return new RpcInvoker(this.broker, ctrl);
        } 

        this.httpClient = new MqClient(this.serverAddress); 
        return new RpcInvoker(this.httpClient, ctrl);  
    }
    close() { 
        if(this.broker != null){
            this.broker.close();
            this.broker = null;
        }
        if(this.httpClient != null){
            this.httpClient.close();
            this.httpClient = null;
        }
    }
}
  
if (__NODEJS__) {
    module.exports.Protocol = Protocol; 
    module.exports.MqClient = MqClient;
    module.exports.ServerAddress = ServerAddress;
    module.exports.BrokerRouteTable = BrokerRouteTable;
    module.exports.Broker = Broker;
    module.exports.Producer = Producer;
    module.exports.Consumer = Consumer;
    module.exports.RpcInvoker = RpcInvoker;
    module.exports.RpcProcessor = RpcProcessor;
    module.exports.ServiceBootstrap = ServiceBootstrap;
    module.exports.ClientBootstrap = ClientBootstrap;
    module.exports.Logger = Logger;
    module.exports.logger = logger;
}

 




© 2015 - 2025 Weber Informatics LLC | Privacy Policy