Modules (181)

NodeConnection

Description

Dependencies

Variables

CONNECTION_ATTEMPTS

Connection attempts to make before failing

Type
number
    var CONNECTION_ATTEMPTS = 10;

CONNECTION_TIMEOUT

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.

Type
number
    var CONNECTION_TIMEOUT  = 10000; // 10 seconds

MAX_COUNTER_VALUE

Maximum value of the command ID counter

Type
number
    var MAX_COUNTER_VALUE = 4294967295; // 2^32 - 1

RETRY_DELAY

Milliseconds to wait before retrying connecting

Type
number
    var RETRY_DELAY         = 500;   // 1/2 second

Functions

Private

attemptSingleConnect

    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();
    }
Private

setDeferredTimeout

    function setDeferredTimeout(deferred, delay) {
        var timer = setTimeout(function () {
            deferred.reject("timeout");
        }, delay);
        deferred.always(function () { clearTimeout(timer); });
    }

Classes

Constructor

NodeConnection

Provides an interface for interacting with the node server.

    function NodeConnection() {
        this.domains = {};
        this._registeredModules = [];
        this._pendingInterfaceRefreshDeferreds = [];
        this._pendingCommandDeferreds = [];
    }
    EventDispatcher.makeEventDispatcher(NodeConnection.prototype);

Properties

Private

_autoReconnect

Type
boolean
    NodeConnection.prototype._autoReconnect = false;
Private

_commandCount

Type
number
    NodeConnection.prototype._commandCount = 1;
Private

_pendingCommandDeferreds

Type
Array.<jQuery.Deferred>
    NodeConnection.prototype._pendingCommandDeferreds = null;
Private

_pendingInterfaceRefreshDeferreds

Type
Array.<jQuery.Deferred>
    NodeConnection.prototype._pendingInterfaceRefreshDeferreds = null;
Private

_port

Type
?number
    NodeConnection.prototype._port = null;
Private

_registeredModules

Type
Array.<string>
    NodeConnection.prototype._registeredModules = null;
Private

_ws

Type
WebSocket
    NodeConnection.prototype._ws = null;

domains

Type
Object
    NodeConnection.prototype.domains = null;

Methods

Private

_cleanup

    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;
    };
Private

_getConnectionTimeout STATIC

Returns: number
Timeout value in milliseconds
    NodeConnection._getConnectionTimeout = function () {
        return CONNECTION_TIMEOUT;
    };

    module.exports = NodeConnection;

});
Private

_getNextCommandID

Returns: number
The next command ID to use. Always representable as an unsigned 32-bit integer.
    NodeConnection.prototype._getNextCommandID = function () {
        var nextID;

        if (this._commandCount > MAX_COUNTER_VALUE) {
            nextID = this._commandCount = 0;
        } else {
            nextID = this._commandCount++;
        }

        return nextID;
    };
Private

_receive

message WebSocket.Message
Message object from WebSocket
    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);
        }
    };
Private

_refreshInterface

    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();
    };
Private

_send

m Object,string
Object to send. Must be JSON.stringify-able.
    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

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.

autoReconnect boolean
Whether to automatically try to reconnect to the server if the connection succeeds and then later disconnects. Note if this connection fails initially, the autoReconnect flag is set to false. Future calls to connect() can reset it to true
Returns: jQuery.Promise
Promise that resolves/rejects when the connection succeeds/fails
    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();
    };

connected

Determines whether the NodeConnection is currently connected

Returns: boolean
Whether the NodeConnection is connected.
    NodeConnection.prototype.connected = function () {
        return !!(this._ws && this._ws.readyState === WebSocket.OPEN);
    };

disconnect

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();
    };

loadDomains

Load domains into the server by path

List Array.<string>
of absolute paths to load
autoReload boolean
Whether to auto-reload the domains if the server fails and restarts. Note that the reload is initiated by the client, so it will only happen after the client reconnects.
Returns: jQuery.Promise
Promise that resolves after the load has succeeded and the new API is availale at NodeConnection.domains, or that rejects on failure.
    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();
    };