2017-02-02 07:12:14 -05:00
2014-06-23 12:47:08 -04:00
define [ ] , () ->
2015-10-14 09:15:33 -04:00
ONEHOUR = 1000 * 60 * 60
2014-06-23 12:47:08 -04:00
class ConnectionManager
2015-10-14 09:15:33 -04:00
2015-10-15 06:38:23 -04:00
disconnectAfterMs: ONEHOUR * 24
2015-10-14 09:15:33 -04:00
lastUserAction : new Date ( )
2014-06-23 12:47:08 -04:00
constructor: (@ide, @$scope) ->
2015-03-09 09:57:13 -04:00
if ! io ?
console . error " Socket.io javascript not loaded. Please check that the real-time service is running and accessible. "
@ide.socket =
on : () ->
$scope . $apply () =>
@$scope.state.error = " Could not connect to websocket server :( "
return
2014-06-24 11:33:36 -04:00
2015-10-14 09:15:33 -04:00
setInterval ( () =>
@ disconnectIfInactive ( )
, ONEHOUR )
2016-11-03 08:27:19 -04:00
# trigger a reconnect immediately if network comes back online
window . addEventListener ' online ' , =>
sl_console . log " [online] browser notified online "
if ! @ connected
@ tryReconnectWithRateLimit ( { force : true } )
2015-10-14 11:25:48 -04:00
@userIsLeavingPage = false
window . addEventListener ' beforeunload ' , =>
@userIsLeavingPage = true
2015-11-05 05:18:40 -05:00
return # Don't return true or it will show a pop up
2015-10-14 11:25:48 -04:00
2015-10-14 09:15:33 -04:00
@connected = false
@userIsInactive = false
2016-10-24 11:36:40 -04:00
@gracefullyReconnecting = false
2016-10-31 12:26:08 -04:00
@$scope.connection =
2014-06-24 11:33:36 -04:00
reconnecting: false
# If we need to force everyone to reload the editor
forced_disconnect: false
2015-10-14 09:15:33 -04:00
inactive_disconnect: false
2014-06-24 11:33:36 -04:00
@$scope.tryReconnectNow = () =>
2016-10-31 11:32:01 -04:00
# user manually requested reconnection via "Try now" button
@ tryReconnectWithRateLimit ( { force : true } )
2014-06-24 11:33:36 -04:00
2015-09-29 05:23:21 -04:00
@ $scope . $on ' cursor:editor:update ' , () =>
2016-10-31 12:19:07 -04:00
@lastUserAction = new Date ( ) # time of last edit
2015-09-28 10:45:14 -04:00
if ! @ connected
2016-10-31 11:32:01 -04:00
# user is editing, try to reconnect
@ tryReconnectWithRateLimit ( )
2015-09-28 11:18:09 -04:00
2015-09-29 05:23:21 -04:00
document . querySelector ( ' body ' ) . addEventListener ' click ' , (e) =>
if ! @ connected and e . target . id != ' try-reconnect-now-button '
2016-10-31 11:32:01 -04:00
# user is editing, try to reconnect
@ tryReconnectWithRateLimit ( )
2014-07-09 15:32:03 -04:00
2015-03-20 15:08:35 -04:00
@ide.socket = io . connect null ,
2014-06-23 12:47:08 -04:00
reconnect: false
2015-10-04 19:43:37 -04:00
' connect timeout ' : 30 * 1000
2014-06-23 12:47:08 -04:00
" force new connection " : true
2016-10-28 11:21:31 -04:00
# The "connect" event is the first event we get back. It only
# indicates that the websocket is connected, we still need to
# pass authentication to join a project.
2014-06-23 12:47:08 -04:00
@ ide . socket . on " connect " , () =>
2016-05-27 09:39:33 -04:00
sl_console . log " [socket.io connect] Connected "
2016-10-28 11:21:31 -04:00
# The next event we should get is an authentication response
# from the server, either "connectionAccepted" or
# "connectionRejected".
@ ide . socket . on ' connectionAccepted ' , (message) =>
sl_console . log " [socket.io connectionAccepted] allowed to connect "
2014-06-24 11:33:36 -04:00
@connected = true
2016-10-24 11:36:40 -04:00
@gracefullyReconnecting = false
2014-06-24 11:33:36 -04:00
@ ide . pushEvent ( " connected " )
2014-06-23 12:47:08 -04:00
@ $scope . $apply () =>
2014-06-24 11:33:36 -04:00
@$scope.connection.reconnecting = false
2015-10-14 09:15:33 -04:00
@$scope.connection.inactive_disconnect = false
2014-06-24 11:33:36 -04:00
if @ $scope . state . loading
2014-07-10 08:41:54 -04:00
@$scope.state.load_progress = 70
2014-06-24 11:33:36 -04:00
2016-10-28 11:21:31 -04:00
# we have passed authentication so we can now join the project
2014-06-24 11:33:36 -04:00
setTimeout ( () =>
@ joinProject ( )
, 100 )
2016-10-28 11:21:31 -04:00
@ ide . socket . on ' connectionRejected ' , (err) =>
sl_console . log " [socket.io connectionRejected] session not valid or other connection error "
# we have failed authentication, usually due to an invalid session cookie
return @ reportConnectionError ( err )
# Alternatively the attempt to connect can fail completely, so
# we never get into the "connect" state.
2015-07-31 10:42:47 -04:00
@ ide . socket . on " connect_failed " , () =>
@connected = false
$scope . $apply () =>
2016-11-04 06:25:08 -04:00
@$scope.state.error = " Unable to connect, please view the <u><a href= ' /learn/Kb/Connection_problems ' >connection problems guide</a></u> to fix the issue. "
2015-07-31 10:42:47 -04:00
2016-10-28 11:21:31 -04:00
# We can get a "disconnect" event at any point after the
# "connect" event.
2015-07-31 10:42:47 -04:00
2014-06-24 11:33:36 -04:00
@ ide . socket . on ' disconnect ' , () =>
2016-05-27 09:39:33 -04:00
sl_console . log " [socket.io disconnect] Disconnected "
2014-06-24 11:33:36 -04:00
@connected = false
@ ide . pushEvent ( " disconnected " )
@ $scope . $apply () =>
@$scope.connection.reconnecting = false
2016-10-24 11:36:40 -04:00
if ! $scope . connection . forced_disconnect and ! @ userIsInactive and ! @ gracefullyReconnecting
2014-06-24 11:33:36 -04:00
@ startAutoReconnectCountdown ( )
2014-06-23 12:47:08 -04:00
2016-10-28 11:21:31 -04:00
# Site administrators can send the forceDisconnect event to all users
2014-06-24 11:33:36 -04:00
@ ide . socket . on ' forceDisconnect ' , (message) =>
@ $scope . $apply () =>
2014-07-24 10:59:24 -04:00
@$scope.permissions.write = false
2014-06-24 11:33:36 -04:00
@$scope.connection.forced_disconnect = true
2014-07-24 11:39:32 -04:00
@ ide . socket . disconnect ( )
2014-07-24 10:59:24 -04:00
@ ide . showGenericMessageModal ( " Please Refresh " , """
We ' re performing maintenance on ShareLaTeX and you need to refresh the editor.
Sorry for any inconvenience .
The editor will refresh in automatically in 10 seconds .
""" )
setTimeout () ->
location . reload ( )
, 10 * 1000
2016-10-28 11:21:31 -04:00
2016-10-24 11:36:40 -04:00
@ ide . socket . on " reconnectGracefully " , () =>
sl_console . log " Reconnect gracefully "
@ reconnectGracefully ( )
2016-10-28 11:21:31 -04:00
# Error reporting, which can reload the page if appropriate
reportConnectionError: (err) ->
sl_console . log " [socket.io] reporting connection error "
if err ? . message == " not authorized " or err ? . message == " invalid session "
window . location = " /login?redir= #{ encodeURI ( window . location . pathname ) } "
else
@ ide . socket . disconnect ( )
@ ide . showGenericMessageModal ( " Something went wrong connecting " , """
2017-04-10 07:03:39 -04:00
Something went wrong connecting to your project . Please refresh if this continues to happen .
2016-10-28 11:21:31 -04:00
""" )
2014-06-24 11:33:36 -04:00
joinProject: () ->
2016-05-26 08:54:34 -04:00
sl_console . log " [joinProject] joining... "
2016-10-28 11:21:31 -04:00
# Note: if the "joinProject" message doesn't reach the server
# (e.g. if we are in a disconnected state at this point) the
# callback will never be executed
2017-09-29 11:32:07 -04:00
data = {
2014-06-24 11:33:36 -04:00
project_id: @ ide . project_id
2017-09-29 11:32:07 -04:00
}
2017-10-20 05:10:21 -04:00
if window . anonymousAccessToken
data.anonymousAccessToken = window . anonymousAccessToken
2017-09-29 11:32:07 -04:00
@ ide . socket . emit ' joinProject ' , data , (err, project, permissionsLevel, protocolVersion) =>
2017-04-10 07:03:39 -04:00
if err ? or ! project ?
2016-10-28 11:21:31 -04:00
return @ reportConnectionError ( err )
2016-05-31 06:47:48 -04:00
2014-06-24 11:33:36 -04:00
if @ $scope . protocolVersion ? and @ $scope . protocolVersion != protocolVersion
location . reload ( true )
2014-06-23 12:47:08 -04:00
2014-06-24 11:33:36 -04:00
@ $scope . $apply () =>
@$scope.protocolVersion = protocolVersion
@$scope.project = project
2014-07-03 10:05:35 -04:00
@$scope.permissionsLevel = permissionsLevel
2014-06-24 11:33:36 -04:00
@$scope.state.load_progress = 100
@$scope.state.loading = false
2014-07-08 05:08:38 -04:00
@ $scope . $broadcast " project:joined "
2014-06-24 10:31:44 -04:00
reconnectImmediately: () ->
@ disconnect ( )
2014-06-24 11:33:36 -04:00
@ tryReconnect ( )
2014-06-24 10:31:44 -04:00
disconnect: () ->
2016-10-31 11:33:09 -04:00
sl_console . log " [socket.io] disconnecting client "
2014-06-24 11:33:36 -04:00
@ ide . socket . disconnect ( )
startAutoReconnectCountdown: () ->
2016-10-31 11:33:09 -04:00
sl_console . log " [ConnectionManager] starting autoreconnect countdown "
2014-06-24 11:56:31 -04:00
twoMinutes = 2 * 60 * 1000
2016-10-31 12:19:07 -04:00
if @ lastUserAction ? and new Date ( ) - @ lastUserAction > twoMinutes
2014-06-24 11:56:31 -04:00
# between 1 minute and 3 minutes
countdown = 60 + Math . floor ( Math . random ( ) * 120 )
else
countdown = 3 + Math . floor ( Math . random ( ) * 7 )
2015-10-14 11:25:48 -04:00
if @ userIsLeavingPage #user will have pressed refresh or back etc
return
2014-06-24 11:33:36 -04:00
@ $scope . $apply () =>
2014-07-02 11:41:29 -04:00
@$scope.connection.reconnecting = false
2014-06-24 11:56:31 -04:00
@$scope.connection.reconnection_countdown = countdown
2014-06-24 11:33:36 -04:00
setTimeout ( =>
if ! @ connected
@timeoutId = setTimeout ( => @ decreaseCountdown ( ) ) , 1000
, 200 )
cancelReconnect: () ->
2016-10-31 11:31:32 -04:00
# clear timeout and set to null so we know there is no countdown running
if @ timeoutId ?
sl_console . log " [ConnectionManager] cancelling existing reconnect timer "
clearTimeout @ timeoutId
@timeoutId = null
2014-06-24 11:33:36 -04:00
decreaseCountdown: () ->
2016-10-31 11:31:32 -04:00
@timeoutId = null
2014-06-24 12:44:46 -04:00
return if ! @ $scope . connection . reconnection_countdown ?
2016-10-31 11:31:32 -04:00
sl_console . log " [ConnectionManager] decreasing countdown " , @ $scope . connection . reconnection_countdown
2014-06-24 11:33:36 -04:00
@ $scope . $apply () =>
@ $scope . connection . reconnection_countdown - -
if @ $scope . connection . reconnection_countdown <= 0
@ $scope . $apply () =>
@ tryReconnect ( )
else
@timeoutId = setTimeout ( => @ decreaseCountdown ( ) ) , 1000
tryReconnect: () ->
2016-10-31 11:33:09 -04:00
sl_console . log " [ConnectionManager] tryReconnect "
2014-06-24 11:33:36 -04:00
@ cancelReconnect ( )
2014-07-17 08:46:14 -04:00
delete @ $scope . connection . reconnection_countdown
2014-07-17 08:44:50 -04:00
return if @ connected
2014-06-24 11:33:36 -04:00
@$scope.connection.reconnecting = true
2016-10-31 11:27:26 -04:00
# use socket.io connect() here to make a single attempt, the
# reconnect() method makes multiple attempts
@ ide . socket . socket . connect ( )
2016-10-31 11:32:01 -04:00
# record the time of the last attempt to connect
@lastConnectionAttempt = new Date ( )
2014-06-24 12:44:46 -04:00
setTimeout ( => @ startAutoReconnectCountdown ( ) if ! @ connected ) , 2000
2014-06-24 11:33:36 -04:00
2016-11-03 08:25:36 -04:00
MIN_RETRY_INTERVAL: 1000 # ms, rate limit on reconnects for user clicking "try now"
BACKGROUND_RETRY_INTERVAL : 5 * 1000 # ms, rate limit on reconnects for other user activity (e.g. cursor moves)
2016-10-31 11:32:01 -04:00
tryReconnectWithRateLimit: (options) ->
# bail out if the reconnect is already in progress
return if @ $scope . connection ? . reconnecting
# bail out if we are going to reconnect soon anyway
reconnectingSoon = @ $scope . connection ? . reconnection_countdown ? and @ $scope . connection . reconnection_countdown <= 5
clickedTryNow = options ? . force # user requested reconnection
return if reconnectingSoon and not clickedTryNow
# bail out if we tried reconnecting recently
allowedInterval = if clickedTryNow then @ MIN_RETRY_INTERVAL else @ BACKGROUND_RETRY_INTERVAL
return if @ lastConnectionAttempt ? and new Date ( ) - @ lastConnectionAttempt < allowedInterval
@ tryReconnect ( )
2015-10-14 09:15:33 -04:00
disconnectIfInactive: ()->
2015-10-15 06:38:23 -04:00
@userIsInactive = ( new Date ( ) - @ lastUserAction ) > @ disconnectAfterMs
2015-10-14 09:15:33 -04:00
if @ userIsInactive and @ connected
@ disconnect ( )
@ $scope . $apply () =>
@$scope.connection.inactive_disconnect = true
2016-10-24 11:36:40 -04:00
RECONNECT_GRACEFULLY_RETRY_INTERVAL: 5000 # ms
MAX_RECONNECT_GRACEFULLY_INTERVAL: 60 * 5 * 1000 # 5 minutes
reconnectGracefully: () ->
@ reconnectGracefullyStarted ? = new Date ( )
userIsInactive = ( new Date ( ) - @ lastUserAction ) > @ RECONNECT_GRACEFULLY_RETRY_INTERVAL
maxIntervalReached = ( new Date ( ) - @ reconnectGracefullyStarted ) > @ MAX_RECONNECT_GRACEFULLY_INTERVAL
if userIsInactive or maxIntervalReached
sl_console . log " [reconnectGracefully] User didn ' t do anything for last 5 seconds, reconnecting "
@ _reconnectGracefullyNow ( )
else
sl_console . log " [reconnectGracefully] User is working, will try again in 5 seconds "
setTimeout () =>
@ reconnectGracefully ( )
, @ RECONNECT_GRACEFULLY_RETRY_INTERVAL
2016-10-31 12:26:08 -04:00
2016-10-24 11:36:40 -04:00
_reconnectGracefullyNow: () ->
@gracefullyReconnecting = true
@reconnectGracefullyStarted = null
2016-10-25 09:09:36 -04:00
# Clear cookie so we don't go to the same backend server
$ . cookie ( " SERVERID " , " " , { expires: - 1 , path: " / " } )
2016-10-28 11:21:31 -04:00
@ reconnectImmediately ( )