mirror of
https://github.com/zulip/zulip.git
synced 2025-11-06 15:03:34 +00:00
We've only gotten one error report for this error, and it didn't have a stack trace, so it's hard to know whether this will actually fix the issue or not. (imported from commit 7bff052347a33f67d8479b1cb23f6e5e24defc75)
283 lines
11 KiB
JavaScript
283 lines
11 KiB
JavaScript
function Socket(url) {
|
|
this.url = url;
|
|
this._is_open = false;
|
|
this._is_authenticated = false;
|
|
this._is_reconnecting = false;
|
|
this._reconnect_initiation_time = null;
|
|
this._send_queue = [];
|
|
this._next_req_id = 0;
|
|
this._requests = {};
|
|
this._connection_failures = 0;
|
|
this._reconnect_timeout_id = null;
|
|
this._heartbeat_timeout_id = null;
|
|
|
|
var that = this;
|
|
this._is_unloading = false;
|
|
$(window).on("unload", function () {
|
|
that._is_unloading = true;
|
|
});
|
|
|
|
$(document).on("unsuspend", function () {
|
|
that._try_to_reconnect();
|
|
});
|
|
|
|
this._supported_protocols = ['websocket', 'xdr-streaming', 'xhr-streaming',
|
|
'xdr-polling', 'xhr-polling', 'jsonp-polling'];
|
|
if (page_params.test_suite) {
|
|
this._supported_protocols = _.reject(this._supported_protocols,
|
|
function (x) { return x === 'xhr-streaming'; });
|
|
}
|
|
|
|
this._sockjs = new SockJS(url, null, {protocols_whitelist: this._supported_protocols});
|
|
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});
|
|
this._try_to_reconnect();
|
|
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;
|
|
var that = this;
|
|
this._next_req_id++;
|
|
this._requests[req_id] = {type: type, request: msg, success: success,
|
|
error: error};
|
|
this._requests[req_id].ack_timeout_id = setTimeout(function () {
|
|
blueslip.info("Timeout on ACK for request " + req_id);
|
|
that._try_to_reconnect();
|
|
}, 2000);
|
|
|
|
try {
|
|
this._sockjs.send(JSON.stringify({req_id: req_id,
|
|
type: type, request: msg}));
|
|
} catch (e) {
|
|
if (e instanceof Error && e.message === 'INVALID_STATE_ERR') {
|
|
// The connection was somehow closed. Our on-close handler will
|
|
// be called imminently and we'll retry this request upon reconnect.
|
|
return;
|
|
} else if (e instanceof Error && e.message.indexOf("NS_ERROR_NOT_CONNECTED") !== -1) {
|
|
// This is a rarely-occurring Firefox error. I'm not sure
|
|
// whether our on-close handler will be called, so let's just
|
|
// call close() explicitly.
|
|
this._sockjs.close();
|
|
return;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
},
|
|
|
|
_can_send: function Socket__can_send() {
|
|
return this._is_open && this._is_authenticated;
|
|
},
|
|
|
|
_resend: function Socket__resend(req_id) {
|
|
var req_info = this._requests[req_id];
|
|
if (req_info.ack_timeout_id !== null) {
|
|
clearTimeout(req_info.ack_timeout_id);
|
|
req_info.ack_timeout_id = null;
|
|
}
|
|
delete this._requests[req_id];
|
|
|
|
if (req_info.type !== 'request') {
|
|
blueslip.error("Cannot resend message of type: " + req_info.type);
|
|
return;
|
|
}
|
|
|
|
this.send(req_info.request, req_info.success, req_info.error);
|
|
},
|
|
|
|
_drain_queue: function Socket__drain_queue() {
|
|
var that = this;
|
|
var queue = this._send_queue;
|
|
this._send_queue = [];
|
|
_.each(queue, function (elem) {
|
|
that.send(elem.msg, elem.success, elem.error);
|
|
});
|
|
},
|
|
|
|
_process_response: function Socket__process_response(req_id, response) {
|
|
var req_info = this._requests[req_id];
|
|
if (req_info === undefined) {
|
|
if (req_id >= this._next_req_id) {
|
|
blueslip.error("Got a response for an unknown request",
|
|
{request_id: req_id, next_id: this._next_req_id,
|
|
outstanding_ids: _.keys(this._requests)});
|
|
}
|
|
// There is a small race where we might start reauthenticating
|
|
// before one of our requests has finished but then have the request
|
|
// finish and thus receive the finish notification both from the
|
|
// status inquiry and from the normal response. Therefore, we might
|
|
// be processing the response for a request where we already got the
|
|
// response from a status inquiry. In that case, don't process the
|
|
// response twice.
|
|
return;
|
|
}
|
|
|
|
if (response.result === 'success') {
|
|
req_info.success(response);
|
|
} else {
|
|
req_info.error('response', response);
|
|
}
|
|
delete this._requests[req_id];
|
|
},
|
|
|
|
_process_ack: function Socket__process_ack(req_id) {
|
|
var req_info = this._requests[req_id];
|
|
if (req_info === undefined) {
|
|
blueslip.error("Got an ACK for an unknown request",
|
|
{request_id: req_id, next_id: this._next_req_id,
|
|
outstanding_ids: _.keys(this._requests)});
|
|
return;
|
|
}
|
|
|
|
if (req_info.ack_timeout_id !== null) {
|
|
clearTimeout(req_info.ack_timeout_id);
|
|
req_info.ack_timeout_id = null;
|
|
}
|
|
},
|
|
|
|
_setup_sockjs_callbacks: function Socket__setup_sockjs_callbacks(sockjs) {
|
|
var that = this;
|
|
sockjs.onopen = function Socket__sockjs_onopen() {
|
|
blueslip.info("Socket connected [transport=" + sockjs.protocol + "]");
|
|
if (that._reconnect_initiation_time !== null) {
|
|
// If this is a reconnect, network was probably
|
|
// recently interrupted, so we optimistically restart
|
|
// get_updates
|
|
restart_get_updates();
|
|
}
|
|
that._is_open = true;
|
|
|
|
// Notify listeners that we've finished the websocket handshake
|
|
$(document).trigger($.Event('websocket_postopen.zulip', {}));
|
|
|
|
// We can only authenticate after the DOM has loaded because we need
|
|
// the CSRF token
|
|
$(function () {
|
|
that._do_send('auth', {csrf_token: csrf_token,
|
|
queue_id: page_params.event_queue_id,
|
|
status_inquiries: _.keys(that._requests)},
|
|
function (resp) {
|
|
that._is_authenticated = true;
|
|
that._is_reconnecting = false;
|
|
that._reconnect_initiation_time = null;
|
|
that._connection_failures = 0;
|
|
_.each(resp.status_inquiries, function (status, id) {
|
|
if (status.status === 'complete') {
|
|
that._process_response(id, status.response);
|
|
}
|
|
if (status.status === 'not_received') {
|
|
that._resend(id);
|
|
}
|
|
});
|
|
that._drain_queue();
|
|
},
|
|
function (type, resp) {
|
|
blueslip.info("Could not authenticate with server: " + resp.msg);
|
|
that._connection_failures++;
|
|
that._try_to_reconnect(that._reconnect_wait_time());
|
|
});
|
|
});
|
|
};
|
|
|
|
sockjs.onmessage = function Socket__sockjs_onmessage(event) {
|
|
if (event.data.type === 'ack') {
|
|
that._process_ack(event.data.req_id);
|
|
} else {
|
|
that._process_response(event.data.req_id, event.data.response);
|
|
}
|
|
};
|
|
|
|
sockjs.onheartbeat = function Socket__socjks_onheartbeat() {
|
|
if (that._heartbeat_timeout_id !== null) {
|
|
clearTimeout(that._heartbeat_timeout_id);
|
|
that._heartbeat_timeout_id = null;
|
|
}
|
|
that._heartbeat_timeout_id = setTimeout(function () {
|
|
that._heartbeat_timeout_id = null;
|
|
blueslip.info("Missed too many hearbeats");
|
|
that._try_to_reconnect();
|
|
}, 60000);
|
|
};
|
|
|
|
sockjs.onclose = function Socket__sockjs_onclose() {
|
|
if (that._is_unloading) {
|
|
return;
|
|
}
|
|
// We've failed to handshake, but notify that the attempt finished
|
|
$(document).trigger($.Event('websocket_postopen.zulip', {}));
|
|
|
|
blueslip.info("SockJS connection lost. Attempting to reconnect soon.");
|
|
that._connection_failures++;
|
|
that._is_reconnecting = false;
|
|
that._try_to_reconnect(that._reconnect_wait_time());
|
|
};
|
|
},
|
|
|
|
_reconnect_wait_time: function Socket__reconnect_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
|
|
return 30;
|
|
} else {
|
|
return Math.min(90, Math.exp(this._connection_failures/2)) * 1000;
|
|
}
|
|
},
|
|
|
|
_try_to_reconnect: function Socket__try_to_reconnect(wait_time) {
|
|
if (wait_time === undefined) {
|
|
wait_time = 0;
|
|
}
|
|
var that = this;
|
|
|
|
var now = (new Date()).getTime();
|
|
if (this._is_reconnecting && now - this._reconnect_initiation_time < 1000) {
|
|
// Only try to reconnect once a second
|
|
return;
|
|
}
|
|
|
|
if (this._reconnect_timeout_id !== null) {
|
|
clearTimeout(this._reconnect_timeout_id);
|
|
this._reconnect_timeout_id = null;
|
|
}
|
|
|
|
if (this._heartbeat_timeout_id !== null) {
|
|
clearTimeout(that._heartbeat_timeout_id);
|
|
this._heartbeat_timeout_id = null;
|
|
}
|
|
|
|
// Cancel any pending auth requests and any timeouts for ACKs
|
|
_.each(this._requests, function (val, key) {
|
|
if (val.ack_timeout_id !== null) {
|
|
clearTimeout(val.ack_timeout_id);
|
|
val.ack_timeout_id = null;
|
|
}
|
|
|
|
if (val.type === 'auth') {
|
|
delete that._requests[key];
|
|
}
|
|
});
|
|
|
|
this._is_open = false;
|
|
this._is_authenticated = false;
|
|
this._is_reconnecting = true;
|
|
this._reconnect_initiation_time = now;
|
|
this._sockjs.close();
|
|
|
|
this._reconnect_timeout_id = setTimeout(function () {
|
|
that._reconnect_timeout_id = null;
|
|
blueslip.info("Attempting socket reconnect.");
|
|
that._sockjs = new SockJS(that.url, null, {protocols_whitelist: that._supported_protocols});
|
|
that._setup_sockjs_callbacks(that._sockjs);
|
|
}, wait_time);
|
|
}
|
|
};
|