From 8c21ef2c0b258ea39da0249adeedbbfda4f8d66d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 6 Jan 2015 17:48:05 -0800 Subject: [PATCH] Implement JX.WebSocket Summary: Ref T6559. Wraps WebSocket in a reasonable driver class which does event dispatch, some state management, and handles automatic reconnect. Test Plan: In Safari, Firefox and Chrome, connected to a websocket server and sent messages back and forth. Terminated and restarted server, saw automatic reconnects successfully reestablish a connection on all browsers. Reviewers: btrahan, joshuaspence Reviewed By: joshuaspence Subscribers: epriestley Maniphest Tasks: T6559 Differential Revision: https://secure.phabricator.com/D11252 --- resources/celerity/map.php | 5 + .../rsrc/externals/javelin/lib/WebSocket.js | 178 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 webroot/rsrc/externals/javelin/lib/WebSocket.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index ede8ed79d3..70eaf7ca44 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -200,6 +200,7 @@ return array( 'rsrc/externals/javelin/lib/Router.js' => '29274e2b', 'rsrc/externals/javelin/lib/URI.js' => '6eff08aa', 'rsrc/externals/javelin/lib/Vector.js' => 'cc1bd0b0', + 'rsrc/externals/javelin/lib/WebSocket.js' => '897b80bf', 'rsrc/externals/javelin/lib/Workflow.js' => 'd149e002', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', @@ -685,6 +686,7 @@ return array( 'javelin-view-interpreter' => '0c33c1a0', 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', + 'javelin-websocket' => '897b80bf', 'javelin-workflow' => 'd149e002', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => '8f380ebc', @@ -1360,6 +1362,9 @@ return array( 'javelin-view-interpreter', 'javelin-view-renderer', ), + '897b80bf' => array( + 'javelin-install', + ), '8a41885b' => array( 'javelin-install', 'javelin-dom', diff --git a/webroot/rsrc/externals/javelin/lib/WebSocket.js b/webroot/rsrc/externals/javelin/lib/WebSocket.js new file mode 100644 index 0000000000..6dd8749e12 --- /dev/null +++ b/webroot/rsrc/externals/javelin/lib/WebSocket.js @@ -0,0 +1,178 @@ +/** + * @requires javelin-install + * @provides javelin-websocket + * @javelin + */ + +/** + * Wraps a WebSocket. + */ +JX.install('WebSocket', { + + construct: function(uri) { + this.setURI(uri); + }, + + properties: { + URI: null, + + /** + * Called when a connection is established or re-established after an + * interruption. + */ + openHandler: null, + + /** + * Called when a message is received. + */ + messageHandler: null, + + /** + * Called when the connection is closed. + * + * You can return `true` to prevent the socket from reconnecting. + */ + closeHandler: null + }, + + members: { + /** + * The underlying WebSocket. + */ + _socket: null, + + /** + * Is the socket connected? + */ + _isOpen: false, + + /** + * Has the caller asked us to close? + * + * By default, we reconnect when the connection is interrupted. + * This stops us from reconnecting if @{method:close} was called. + */ + _shouldClose: false, + + /** + * Number of milliseconds to wait after a connection failure before + * attempting to reconnect. + */ + _delayUntilReconnect: null, + + + /** + * Open the connection. + */ + open: function() { + if (!window.WebSocket) { + return; + } + + this._shouldClose = false; + this._resetDelay(); + + this._socket = new WebSocket(this.getURI()); + this._socket.onopen = JX.bind(this, this._onopen); + this._socket.onmessage = JX.bind(this, this._onmessage); + this._socket.onclose = JX.bind(this, this._onclose); + }, + + + /** + * Send a message. + * + * If the connection is not currently open, this method has no effect and + * the messages vanishes into the ether. + */ + send: function(message) { + if (this._isOpen) { + this._socket.send(message); + } + }, + + + /** + * Close the connection. + */ + close: function() { + if (!$this._isOpen) { + return; + } + this._shouldClose = true; + this._socket.close(); + }, + + + /** + * Callback for connection open. + */ + _onopen: function(e) { + this._isOpen = true; + + // Reset the reconnect delay, since we connected successfully. + this._resetDelay(); + + var handler = this.getOpenHandler(); + if (handler) { + handler(); + } + }, + + + /** + * Reset the reconnect delay to its base value. + */ + _resetDelay: function() { + this._delayUntilReconnect = 2000; + }, + + + /** + * Callback for message received. + */ + _onmessage: function(e) { + var data = e.data; + + var handler = this.getMessageHandler(); + if (handler) { + handler(data); + } + }, + + + /** + * Callback for connection close. + */ + _onclose: function(e) { + this._isOpen = false; + + var done = false; + + var handler = this.getCloseHandler(); + if (handler) { + done = handler(); + } + + // If we didn't explicitly see a close() call and the close handler + // did not return `true` to stop the reconnect, wait a little while + // and try to reconnect. + if (!done && !this._shouldClose) { + setTimeout(JX.bind(this, this._reconnect), this._delayUntilReconnect); + } + }, + + + /** + * Reconnect an interrupted socket. + */ + _reconnect: function() { + // Increase the reconnect delay by a factor of 2. If we fail to open the + // connection, the close handler will send us back here. We'll reconnect + // more and more slowly until we eventually get a valid connection. + this._delayUntilReconnect = this._delayUntilReconnect * 2; + this.open(); + } + + } +});