EVOLUTION-MANAGER
Edit File: multiplex-client.js
"use strict"; const log = require("./log"); const debug = require("./debug"); // MultiplexClient sits on top of a SockJS connection and lets the caller // open logical SockJS connections (channels). The SockJS connection is // closed when all of the channels close. This means you can't start with // zero channels, open a channel, close that channel, and then open // another channel. module.exports = MultiplexClient; function MultiplexClient(conn) { // The underlying SockJS connection. At this point it is not likely to // be opened yet. this._conn = conn; // A table of all active channels. // Key: id, value: MultiplexClientChannel this._channels = {}; this._channelCount = 0; // ID to use for the next channel that is opened this._nextId = 0; // Channels that need to be opened when the SockJS connection's open // event is received this._pendingChannels = []; // A list of functions that fire when our connection goes away. this.onclose = []; this._conn.onopen = () => { log("Connection opened. " + global.location.href); let channel; while ((channel = this._pendingChannels.shift())) { // Be sure to check readyState so we don't open connections for // channels that were closed before they finished opening if (channel.readyState === 0) { channel._open(); } else { debug("NOT opening channel " + channel.id); } } }; this._conn.onclose = e => { log("Connection closed. Info: " + JSON.stringify(e)); debug("SockJS connection closed"); // If the SockJS connection is terminated from the other end (or due // to loss of connectivity or whatever) then we can notify all the // active channels that they are closed too. for (let key in this._channels) { if (Object.prototype.hasOwnProperty.call(this._channels, key)) { this._channels[key]._destroy(e); } } for (let i = 0; i < this.onclose.length; i++) { this.onclose[i](); } }; this._conn.onmessage = e => { let msg = parseMultiplexData(e.data); if (!msg) { log("Invalid multiplex packet received from server"); this._conn.close(); return; } let id = msg.id; let method = msg.method; let payload = msg.payload; let channel = this._channels[id]; if (!channel) { log("Multiplex channel " + id + " not found"); return; } if (method === "c") { // If we're closing, we want to close everything, not just a subapp. // So don't send to a single channel. this._conn.close(payload.code, payload.reason); } else if (method === "m") { channel.onmessage({data: payload}); } }; } MultiplexClient.prototype.open = function(url) { let channel = new MultiplexClientChannel(this, this._nextId++ + "", this._conn, url); this._channels[channel.id] = channel; this._channelCount++; switch (this._conn.readyState) { case 0: this._pendingChannels.push(channel); break; case 1: setTimeout(() => { channel._open(); }, 0); break; default: setTimeout(() => { channel.close(); }, 0); break; } return channel; }; MultiplexClient.prototype.removeChannel = function(id) { delete this._channels[id]; this._channelCount--; debug("Removed channel " + id + ", " + this._channelCount + " left"); if (this._channelCount === 0 && this._conn.readyState < 2) { debug("Closing SockJS connection since no channels are left"); this._conn.close(); } }; function MultiplexClientChannel(owner, id, conn, url) { this._owner = owner; this.id = id; this.conn = conn; this.url = url; this.readyState = 0; this.onopen = function() {}; this.onclose = function() {}; this.onmessage = function() {}; } MultiplexClientChannel.prototype._open = function(parentURL) { debug("Open channel " + this.id); this.readyState = 1; //let relURL = getRelativePath(parentURL, this.url) this.conn.send(formatOpenEvent(this.id, this.url)); if (this.onopen) this.onopen(); }; MultiplexClientChannel.prototype.send = function(data) { if (this.readyState === 0) throw new Error("Invalid state: can't send when readyState is 0"); if (this.readyState === 1) this.conn.send(formatMessage(this.id, data)); }; MultiplexClientChannel.prototype.close = function(code, reason) { if (this.readyState >= 2) return; debug("Close channel " + this.id); if (this.conn.readyState === 1) { // Is the underlying connection open? Send a close message. this.conn.send(formatCloseEvent(this.id, code, reason)); } this._destroy({code: code, reason: reason, wasClean: true}); }; // Internal version of close that doesn't notify the server MultiplexClientChannel.prototype._destroy = function(e) { // If we haven't already, invoke onclose handler. if (this.readyState !== 3) { this.readyState = 3; debug("Channel " + this.id + " is closed"); setTimeout(() => { this._owner.removeChannel(this.id); if (this.onclose) this.onclose(e); }, 0); } }; function formatMessage(id, message) { return id + '|m|' + message; } function formatOpenEvent(id, url) { return id + '|o|' + url; } function formatCloseEvent(id, code, reason) { return id + '|c|' + JSON.stringify({code: code, reason: reason}); } function parseMultiplexData(msg) { try { let m = /^(\d+)\|(m|o|c)\|([\s\S]*)$/m.exec(msg); if (!m) return null; msg = { id: m[1], method: m[2], payload: m[3] }; switch (msg.method) { case 'm': break; case 'o': if (msg.payload.length === 0) return null; break; case 'c': try { msg.payload = JSON.parse(msg.payload); } catch(e) { return null; } break; default: return null; } return msg; } catch(e) { log('Error parsing multiplex data: ' + e); return null; } }