Connection attempts to make before failing
var CONNECTION_ATTEMPTS = 10;
Milliseconds to wait before a particular connection attempt is considered failed. NOTE: It's okay for the connection timeout to be long because the expected behavior of WebSockets is to send a "close" event as soon as they realize they can't connect. So, we should rarely hit the connection timeout even if we try to connect to a port that isn't open.
var CONNECTION_TIMEOUT = 10000; // 10 seconds
function attemptSingleConnect() {
var deferred = $.Deferred();
var port = null;
var ws = null;
setDeferredTimeout(deferred, CONNECTION_TIMEOUT);
brackets.app.getNodeState(function (err, nodePort) {
if (!err && nodePort && deferred.state() !== "rejected") {
port = nodePort;
ws = new WebSocket("ws://localhost:" + port);
// Expect ArrayBuffer objects from Node when receiving binary
// data instead of DOM Blobs, which are the default.
ws.binaryType = "arraybuffer";
// If the server port isn't open, we get a close event
// at some point in the future (and will not get an onopen
// event)
ws.onclose = function () {
deferred.reject("WebSocket closed");
};
ws.onopen = function () {
// If we successfully opened, remove the old onclose
// handler (which was present to detect failure to
// connect at all).
ws.onclose = null;
deferred.resolveWith(null, [ws, port]);
};
} else {
deferred.reject("brackets.app.getNodeState error: " + err);
}
});
return deferred.promise();
}
Provides an interface for interacting with the node server.
function NodeConnection() {
this.domains = {};
this._registeredModules = [];
this._pendingInterfaceRefreshDeferreds = [];
this._pendingCommandDeferreds = [];
}
EventDispatcher.makeEventDispatcher(NodeConnection.prototype);
NodeConnection.prototype._pendingCommandDeferreds = null;
NodeConnection.prototype._cleanup = function () {
// clear out the domains, since we may get different ones
// on the next connection
this.domains = {};
// shut down the old connection if there is one
if (this._ws && this._ws.readyState !== WebSocket.CLOSED) {
try {
this._ws.close();
} catch (e) { }
}
var failedDeferreds = this._pendingInterfaceRefreshDeferreds
.concat(this._pendingCommandDeferreds);
failedDeferreds.forEach(function (d) {
d.reject("cleanup");
});
this._pendingInterfaceRefreshDeferreds = [];
this._pendingCommandDeferreds = [];
this._ws = null;
this._port = null;
};
NodeConnection._getConnectionTimeout = function () {
return CONNECTION_TIMEOUT;
};
module.exports = NodeConnection;
});
NodeConnection.prototype._getNextCommandID = function () {
var nextID;
if (this._commandCount > MAX_COUNTER_VALUE) {
nextID = this._commandCount = 0;
} else {
nextID = this._commandCount++;
}
return nextID;
};
NodeConnection.prototype._receive = function (message) {
var responseDeferred = null;
var data = message.data;
var m;
if (message.data instanceof ArrayBuffer) {
// The first four bytes encode the command ID as an unsigned 32-bit integer
if (data.byteLength < 4) {
console.error("[NodeConnection] received malformed binary message");
return;
}
var header = data.slice(0, 4),
body = data.slice(4),
headerView = new Uint32Array(header),
id = headerView[0];
// Unpack the binary message into a commandResponse
m = {
type: "commandResponse",
message: {
id: id,
response: body
}
};
} else {
try {
m = JSON.parse(data);
} catch (e) {
console.error("[NodeConnection] received malformed message", message, e.message);
return;
}
}
switch (m.type) {
case "event":
if (m.message.domain === "base" && m.message.event === "newDomains") {
this._refreshInterface();
}
// Event type "domain:event"
EventDispatcher.triggerWithArray(this, m.message.domain + ":" + m.message.event,
m.message.parameters);
break;
case "commandResponse":
responseDeferred = this._pendingCommandDeferreds[m.message.id];
if (responseDeferred) {
responseDeferred.resolveWith(this, [m.message.response]);
delete this._pendingCommandDeferreds[m.message.id];
}
break;
case "commandProgress":
responseDeferred = this._pendingCommandDeferreds[m.message.id];
if (responseDeferred) {
responseDeferred.notifyWith(this, [m.message.message]);
}
break;
case "commandError":
responseDeferred = this._pendingCommandDeferreds[m.message.id];
if (responseDeferred) {
responseDeferred.rejectWith(
this,
[m.message.message, m.message.stack]
);
delete this._pendingCommandDeferreds[m.message.id];
}
break;
case "error":
console.error("[NodeConnection] received error: " +
m.message.message);
break;
default:
console.error("[NodeConnection] unknown event type: " + m.type);
}
};
NodeConnection.prototype._refreshInterface = function () {
var deferred = $.Deferred();
var self = this;
var pendingDeferreds = this._pendingInterfaceRefreshDeferreds;
this._pendingInterfaceRefreshDeferreds = [];
deferred.then(
function () {
pendingDeferreds.forEach(function (d) { d.resolve(); });
},
function (err) {
pendingDeferreds.forEach(function (d) { d.reject(err); });
}
);
function refreshInterfaceCallback(spec) {
function makeCommandFunction(domainName, commandSpec) {
return function () {
var deferred = $.Deferred();
var parameters = Array.prototype.slice.call(arguments, 0);
var id = self._getNextCommandID();
self._pendingCommandDeferreds[id] = deferred;
self._send({id: id,
domain: domainName,
command: commandSpec.name,
parameters: parameters
});
return deferred;
};
}
// TODO: Don't replace the domain object every time. Instead, merge.
self.domains = {};
self.domainEvents = {};
spec.forEach(function (domainSpec) {
self.domains[domainSpec.domain] = {};
domainSpec.commands.forEach(function (commandSpec) {
self.domains[domainSpec.domain][commandSpec.name] =
makeCommandFunction(domainSpec.domain, commandSpec);
});
self.domainEvents[domainSpec.domain] = {};
domainSpec.events.forEach(function (eventSpec) {
var parameters = eventSpec.parameters;
self.domainEvents[domainSpec.domain][eventSpec.name] = parameters;
});
});
deferred.resolve();
}
if (this.connected()) {
$.getJSON("http://localhost:" + this._port + "/api")
.done(refreshInterfaceCallback)
.fail(function (err) { deferred.reject(err); });
} else {
deferred.reject("Attempted to call _refreshInterface when not connected.");
}
return deferred.promise();
};
NodeConnection.prototype._send = function (m) {
if (this.connected()) {
// Convert the message to a string
var messageString = null;
if (typeof m === "string") {
messageString = m;
} else {
try {
messageString = JSON.stringify(m);
} catch (stringifyError) {
console.error("[NodeConnection] Unable to stringify message in order to send: " + stringifyError.message);
}
}
// If we succeded in making a string, try to send it
if (messageString) {
try {
this._ws.send(messageString);
} catch (sendError) {
console.error("[NodeConnection] Error sending message: " + sendError.message);
}
}
} else {
console.error("[NodeConnection] Not connected to node, unable to send.");
}
};
Connect to the node server. After connecting, the NodeConnection object will trigger a "close" event when the underlying socket is closed. If the connection is set to autoReconnect, then the event will also include a jQuery promise for the connection.
NodeConnection.prototype.connect = function (autoReconnect) {
var self = this;
self._autoReconnect = autoReconnect;
var deferred = $.Deferred();
var attemptCount = 0;
var attemptTimestamp = null;
// Called after a successful connection to do final setup steps
function registerHandlersAndDomains(ws, port) {
// Called if we succeed at the final setup
function success() {
self._ws.onclose = function () {
if (self._autoReconnect) {
var $promise = self.connect(true);
self.trigger("close", $promise);
} else {
self._cleanup();
self.trigger("close");
}
};
deferred.resolve();
}
// Called if we fail at the final setup
function fail(err) {
self._cleanup();
deferred.reject(err);
}
self._ws = ws;
self._port = port;
self._ws.onmessage = self._receive.bind(self);
// refresh the current domains, then re-register any
// "autoregister" modules
self._refreshInterface().then(
function () {
if (self._registeredModules.length > 0) {
self.loadDomains(self._registeredModules, false).then(
success,
fail
);
} else {
success();
}
},
fail
);
}
// Repeatedly tries to connect until we succeed or until we've
// failed CONNECTION_ATTEMPT times. After each attempt, waits
// at least RETRY_DELAY before trying again.
function doConnect() {
attemptCount++;
attemptTimestamp = new Date();
attemptSingleConnect().then(
registerHandlersAndDomains, // succeded
function () { // failed this attempt, possibly try again
if (attemptCount < CONNECTION_ATTEMPTS) { //try again
// Calculate how long we should wait before trying again
var now = new Date();
var delay = Math.max(
RETRY_DELAY - (now - attemptTimestamp),
1
);
setTimeout(doConnect, delay);
} else { // too many attempts, give up
deferred.reject("Max connection attempts reached");
}
}
);
}
// Start the connection process
self._cleanup();
doConnect();
return deferred.promise();
};
Determines whether the NodeConnection is currently connected
NodeConnection.prototype.connected = function () {
return !!(this._ws && this._ws.readyState === WebSocket.OPEN);
};
Explicitly disconnects from the server. Note that even if autoReconnect was set to true at connection time, the connection will not reconnect after this call. Reconnection can be manually done by calling connect() again.
NodeConnection.prototype.disconnect = function () {
this._autoReconnect = false;
this._cleanup();
};
Load domains into the server by path
NodeConnection.prototype.loadDomains = function (paths, autoReload) {
var deferred = $.Deferred();
setDeferredTimeout(deferred, CONNECTION_TIMEOUT);
var pathArray = paths;
if (!Array.isArray(paths)) {
pathArray = [paths];
}
if (autoReload) {
Array.prototype.push.apply(this._registeredModules, pathArray);
}
if (this.domains.base && this.domains.base.loadDomainModulesFromPaths) {
this.domains.base.loadDomainModulesFromPaths(pathArray).then(
function (success) { // command call succeeded
if (!success) {
// response from commmand call was "false" so we know
// the actual load failed.
deferred.reject("loadDomainModulesFromPaths failed");
}
// if the load succeeded, we wait for the API refresh to
// resolve the deferred.
},
function (reason) { // command call failed
deferred.reject("Unable to load one of the modules: " + pathArray + (reason ? ", reason: " + reason : ""));
}
);
this._pendingInterfaceRefreshDeferreds.push(deferred);
} else {
deferred.reject("this.domains.base is undefined");
}
return deferred.promise();
};