src/LiveDevelopment/MultiBrowserImpl/transports/node/NodeSocketTransportDomain.js
/*
* Copyright (c) 2014 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*eslint-env node */
/*jslint node: true */
"use strict";
var WebSocketServer = require("ws").Server,
_ = require("lodash");
/**
* @private
* The WebSocket server we listen for incoming connections on.
* @type {?WebSocketServer}
*/
var _wsServer;
/**
* @private
* The Brackets domain manager for registering node extensions.
* @type {?DomainManager}
*/
var _domainManager;
/**
* @private
* The ID that should be allocated to the next client that connects to the transport.
* @type {number}
*/
var _nextClientId = 1;
/**
* @private
* A map of client IDs to the URL and WebSocket for the given ID.
* @type {Object.<number, {id: number, url: string, socket: WebSocket}>}
*/
var _clients = {};
// This must match the port declared in NodeSocketTransport.js.
// TODO: randomize this?
var SOCKET_PORT = 8123;
/**
* @private
* Returns the client info for a given WebSocket, or null if that socket isn't registered.
* @param {WebSocket} ws
* @return {?{id: number, url: string, socket: WebSocket}}
*/
function _clientForSocket(ws) {
return _.find(_clients, function (client) {
return (client.socket === ws);
});
}
/**
* @private
* Creates the WebSocketServer and handles incoming connections.
*/
function _createServer() {
if (!_wsServer) {
// TODO: make port configurable, or use random port
_wsServer = new WebSocketServer({port: SOCKET_PORT});
_wsServer.on("connection", function (ws) {
ws.on("message", function (msg) {
console.log("WebSocketServer - received - " + msg);
var msgObj;
try {
msgObj = JSON.parse(msg);
} catch (e) {
console.error("nodeSocketTransport: Error parsing message: " + msg);
return;
}
// See the comment in NodeSocketTransportRemote.connect() for why we have an extra
// layer of transport-layer message objects surrounding the protocol messaging.
if (msgObj.type === "connect") {
if (!msgObj.url) {
console.error("nodeSocketTransport: Malformed connect message: " + msg);
return;
}
var clientId = _nextClientId++;
_clients[clientId] = {
id: clientId,
url: msgObj.url,
socket: ws
};
console.log("emitting connect event");
_domainManager.emitEvent("nodeSocketTransport", "connect", [clientId, msgObj.url]);
} else if (msgObj.type === "message") {
var client = _clientForSocket(ws);
if (client) {
_domainManager.emitEvent("nodeSocketTransport", "message", [client.id, msgObj.message]);
} else {
console.error("nodeSocketTransport: Couldn't locate client for message: " + msg);
}
} else {
console.error("nodeSocketTransport: Got bad socket message type: " + msg);
}
}).on("error", function (e) {
// TODO: emit error event
var client = _clientForSocket(ws);
console.error("nodeSocketTransport: Error on socket for client " + JSON.stringify(client) + ": " + e);
}).on("close", function () {
var client = _clientForSocket(ws);
if (client) {
_domainManager.emitEvent("nodeSocketTransport", "close", [client.id]);
delete _clients[client.id];
} else {
console.error("nodeSocketTransport: Socket closed, but couldn't locate client");
}
});
}).on("error", function (e) {
// TODO: emit error event
console.error("nodeSocketTransport: Error on live preview server creation: " + e);
});
}
}
/**
* Initializes the socket server.
* @param {string} url
*/
function _cmdStart(url) {
_createServer();
}
/**
* Sends a transport-layer message over the socket.
* @param {number|Array.<number>} idOrArray A client ID or array of client IDs to send the message to.
* @param {string} msgStr The message to send as a JSON string.
*/
function _cmdSend(idOrArray, msgStr) {
if (!Array.isArray(idOrArray)) {
idOrArray = [idOrArray];
}
idOrArray.forEach(function (id) {
var client = _clients[id];
if (!client) {
console.error("nodeSocketTransport: Couldn't find client ID: " + id);
} else {
client.socket.send(msgStr);
}
});
}
/**
* Closes the connection for a given client ID.
* @param {number} clientId
*/
function _cmdClose(clientId) {
var client = _clients[clientId];
if (client) {
client.socket.close();
delete _clients[clientId];
}
}
/**
* Initializes the domain and registers commands.
* @param {DomainManager} domainManager The DomainManager for the server
*/
function init(domainManager) {
_domainManager = domainManager;
if (!domainManager.hasDomain("nodeSocketTransport")) {
domainManager.registerDomain("nodeSocketTransport", {major: 0, minor: 1});
}
domainManager.registerCommand(
"nodeSocketTransport", // domain name
"start", // command name
_cmdStart, // command handler function
false, // this command is synchronous in Node
"Creates the WS server",
[]
);
domainManager.registerCommand(
"nodeSocketTransport", // domain name
"send", // command name
_cmdSend, // command handler function
false, // this command is synchronous in Node
"Sends a message to a given client or list of clients",
[
{name: "idOrArray", type: "number|Array.<number>", description: "id or array of ids to send the message to"},
{name: "message", type: "string", description: "JSON message to send"}
],
[]
);
domainManager.registerCommand(
"nodeSocketTransport", // domain name
"close", // command name
_cmdClose, // command handler function
false, // this command is synchronous in Node
"Closes the connection to a given client",
[
{name: "id", type: "number", description: "id of connection to close"}
],
[]
);
domainManager.registerEvent(
"nodeSocketTransport",
"connect",
[
{name: "clientID", type: "number", description: "ID of live preview page connecting to live development"},
{name: "url", type: "string", description: "URL of page that live preview is connecting from"}
]
);
domainManager.registerEvent(
"nodeSocketTransport",
"message",
[
{name: "clientID", type: "number", description: "ID of live preview page sending message"},
{name: "msg", type: "string", description: "JSON message from client page"}
]
);
domainManager.registerEvent(
"nodeSocketTransport",
"close",
[
{name: "clientID", type: "number", description: "ID of live preview page being closed"}
]
);
}
exports.init = init;