diff --git a/static/js/socket.js b/static/js/socket.js new file mode 100644 index 0000000000..d88684f922 --- /dev/null +++ b/static/js/socket.js @@ -0,0 +1,126 @@ +function Socket(url) { + this.url = url; + this._is_open = false; + this._is_authenticated = false; + this._send_queue = []; + this._next_req_id = 0; + this._requests = {}; + this._connection_failures = 0; + + this._is_unloading = false; + $(window).on("unload", function () { + this._is_unloading = true; + }); + + this._sockjs = new SockJS(url); + this._setup_sockjs_callbacks(this._sockjs); +} + +Socket.prototype = { + send: function Socket_send(msg, success, error) { + if (! this._can_send()) { + this._send_queue.push({msg: msg, success: success, error: error}); + return; + } + + this._do_send('request', msg, success, error); + }, + + _do_send: function Socket__do_send(type, msg, success, error) { + var req_id = this._next_req_id; + this._next_req_id++; + this._requests[req_id] = {success: success, error: error}; + // TODO: I think we might need to catch exceptions here for certain transports + this._sockjs.send(JSON.stringify({client_meta: {req_id: req_id}, + type: type, request: msg})); + }, + + _can_send: function Socket__can_send() { + return this._is_open && this._is_authenticated; + }, + + _drain_queue_send: function Socket__drain_queue_send() { + var that = this; + var queue = this._send_queue; + this._send_queue = []; + _.each(queue, function (elem) { + that.send(elem.msg, elem.success, elem.error); + }); + }, + + _drain_queue_error: function Socket__drain_queue_error() { + var that = this; + var queue = this._send_queue; + this._send_queue = []; + _.each(queue, function (elem) { + elem.error('connection'); + }); + }, + + _setup_sockjs_callbacks: function Socket__setup_sockjs_callbacks(sockjs) { + var that = this; + sockjs.onopen = function Socket__sockjs_onopen() { + that._is_open = true; + + // We can only authenticate after the DOM has loaded because we need + // the CSRF token + $(function () { + that._do_send('auth', {csrf_token: csrf_token}, + function () { + that._is_authenticated = true; + that._connection_failures = 0; + that._drain_queue_send(); + }, + function (type, resp) { + blueslip.info("Could not authenticate with server: " + resp.msg); + that._try_to_reconnect(); + }); + }); + }; + + sockjs.onmessage = function Socket__sockjs_onmessage(event) { + var req_info = that._requests[event.data.client_meta.req_id]; + if (req_info === undefined) { + blueslip.error("Got a response for an unknown request"); + return; + } + + if (event.data.response.result === 'success') { + req_info.success(event.data.response); + } else { + req_info.error('response', event.data.response); + } + }; + + sockjs.onclose = function Socket__sockjs_onclose() { + if (that._is_unloading) { + return; + } + blueslip.info("SockJS connection lost. Attempting to reconnect soon."); + that._try_to_reconnect(); + }; + }, + + _try_to_reconnect: function Socket__try_to_reconnect() { + var that = this; + this._is_open = false; + this._is_authenticated = false; + this._connection_failures++; + this._drain_queue_error(); + + var wait_time; + if (this._connection_failures === 1) { + // We specify a non-zero timeout here so that we don't try to + // immediately reconnect when the page is refreshing + wait_time = 30; + } else { + wait_time = Math.min(90, Math.exp(this._connection_failures/2)) * 1000; + } + + setTimeout(function () { + blueslip.info("Attempting reconnect."); + that._sockjs = new SockJS(that.url); + that._setup_sockjs_callbacks(that._sockjs); + }, wait_time); + } +}; diff --git a/tools/jslint/check-all.js b/tools/jslint/check-all.js index 638e6f3d2b..bcaa2007dd 100644 --- a/tools/jslint/check-all.js +++ b/tools/jslint/check-all.js @@ -4,7 +4,7 @@ var globals = // Third-party libraries ' $ _ jQuery Spinner Handlebars XDate zxcvbn Intl mixpanel Notification' - + ' LazyLoad Dropbox' + + ' LazyLoad Dropbox SockJS' // Node-based unit tests + ' module' @@ -27,7 +27,7 @@ var globals = + ' invite ui util activity timerender MessageList MessageListView blueslip unread stream_list' + ' onboarding message_edit tab_bar emoji popovers navigate message_tour' + ' avatar feature_flags search_suggestion referral stream_color Dict' - + ' Filter summary admin stream_data muting WinChan muting_ui' + + ' Filter summary admin stream_data muting WinChan muting_ui Socket' // colorspace.js + ' colorspace' diff --git a/zproject/settings.py b/zproject/settings.py index 5d9b1e33b2..2ab581831c 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -367,6 +367,7 @@ JS_SPECS = { 'js/reload.js', 'js/notifications_bar.js', 'js/compose_fade.js', + 'js/socket.js', 'js/compose.js', 'js/stream_color.js', 'js/admin.js',